├── .gitignore ├── .yamllint ├── LICENSE.md ├── README.md ├── aws ├── Makefile ├── README.md ├── cleanup.py ├── config.yml ├── deploy-test-policy.yml ├── policy │ ├── application-security.yaml │ ├── application-services.yaml │ ├── compute.yaml │ ├── data-services.yaml │ ├── networking.yaml │ ├── paas.yaml │ ├── security-services.yaml │ └── storage-services.yaml ├── requirements.txt ├── terminator-policy.json ├── terminator.yml ├── terminator │ ├── __init__.py │ ├── application_security.py │ ├── application_services.py │ ├── compute.py │ ├── data_services.py │ ├── networking.py │ ├── paas.py │ ├── security_services.py │ └── storage_services.py └── terminator_lambda.py ├── azure-pipelines.yml ├── constraints.txt ├── hacking ├── README.md └── aws_config │ ├── ci_policies │ ├── setup-iam.yml │ ├── test-policies.yml │ └── test_policies │ ├── compute.yaml │ ├── container-policy.yaml │ ├── database-policy.yaml │ ├── devops-policy.yaml │ ├── networking.yaml │ ├── security-services.yaml │ └── storage.yaml ├── pycodestyle.ini ├── pylint.rc ├── requirements.yml └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /.cache 3 | *.retry 4 | *.pyc 5 | .tox 6 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | rules: 2 | braces: 3 | min-spaces-inside: 0 4 | max-spaces-inside: 0 5 | min-spaces-inside-empty: -1 6 | max-spaces-inside-empty: -1 7 | brackets: 8 | min-spaces-inside: 0 9 | max-spaces-inside: 0 10 | min-spaces-inside-empty: -1 11 | max-spaces-inside-empty: -1 12 | colons: 13 | max-spaces-before: 0 14 | max-spaces-after: 1 15 | commas: 16 | max-spaces-before: 0 17 | min-spaces-after: 1 18 | max-spaces-after: 1 19 | comments: 20 | level: warning 21 | require-starting-space: true 22 | min-spaces-from-content: 2 23 | comments-indentation: 24 | level: warning 25 | document-end: disable 26 | document-start: 27 | present: false 28 | empty-lines: 29 | max: 2 30 | max-start: 0 31 | max-end: 0 32 | hyphens: 33 | max-spaces-after: 1 34 | indentation: 35 | spaces: consistent 36 | indent-sequences: true 37 | check-multi-line-strings: false 38 | key-duplicates: enable 39 | line-length: 40 | max: 160 41 | allow-non-breakable-words: false 42 | allow-non-breakable-inline-mappings: false 43 | new-line-at-end-of-file: enable 44 | new-lines: 45 | type: unix 46 | trailing-spaces: enable 47 | ignore: | 48 | .tox/ 49 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS Terminator 2 | 3 | An AWS Lambda function for cleaning up AWS resources. 4 | 5 | ## Run the sanity tests 6 | 7 | We use `tox` to run the sanity tests: 8 | 9 | ```console 10 | $ tox 11 | ``` 12 | 13 | You can run one specific test with a `-e foo` parameter. Use `tox -av` to list them all: 14 | 15 | ```console 16 | $ tox -e pylint 17 | ``` 18 | -------------------------------------------------------------------------------- /aws/Makefile: -------------------------------------------------------------------------------- 1 | PYTHON3 ?= python3.9 2 | 3 | EXTRA_VARS=ansible_connection=local ansible_python_interpreter=$(shell which python) 4 | RUN_TEST_POLICY_PLAYBOOK=ansible-playbook -i localhost, -e '$(EXTRA_VARS)' deploy-test-policy.yml $(FLAGS) 5 | RUN_TERMINATOR_PLAYBOOK=ansible-playbook -i localhost, -e '$(EXTRA_VARS)' terminator.yml $(FLAGS) 6 | 7 | .PHONY: default 8 | default: 9 | @echo ">>> Deploy Terminator to the Lambda Account" 10 | @echo 11 | @echo "USAGE: make terminator|terminator_lambda AWS_PROFILE=lambda [FLAGS=flags]" 12 | @echo 13 | @echo ">>> Deploy to the Test Account" 14 | @echo 15 | @echo "USAGE: make test_policy AWS_PROFILE=test STAGE=dev|prod [FLAGS=flags]" 16 | 17 | .PHONY: terminator 18 | terminator: 19 | $(RUN_TERMINATOR_PLAYBOOK) 20 | 21 | .PHONY: terminator_lambda 22 | terminator_lambda: 23 | $(RUN_TERMINATOR_PLAYBOOK) --tags=lambda 24 | 25 | .PHONY: test_policy 26 | test_policy: 27 | $(RUN_TEST_POLICY_PLAYBOOK) 28 | -------------------------------------------------------------------------------- /aws/README.md: -------------------------------------------------------------------------------- 1 | # Contributor process 2 | 3 | [This repository](https://github.com/mattclay/aws-terminator) is used by [Ansible](https://github.com/ansible/ansible) to deploy policies for AWS integration tests. 4 | 5 | To enable new integration tests for the CI account, you can start the process by opening a pull request here. 6 | 7 | There are two things you may need to do: 8 | 1. Update the permissions in the [policy directory](https://github.com/mattclay/aws-terminator/tree/master/aws/policy) with those needed to run your integration tests (policy groups are defined below). Check the existing policies to avoid adding duplicates. 9 | The [AWS module developer guidelines](https://docs.ansible.com/ansible/devel/dev_guide/platforms/aws_guidelines.html#aws-permissions-for-integration-tests) contains some tips on finding the minimum IAM policy needed for running the integration tests. 10 | 2. Add a terminator class in the corresponding file in the [terminator directory](https://github.com/mattclay/aws-terminator/tree/master/aws/terminator) if you are adding permissions to create a new type of AWS resource. Skip this step and submit your pull request if you are only adding permissions to modify resources that are already supported. 11 | 12 | The rest of this section is about creating and testing your terminator class. 13 | 14 | If your integration tests fail there could be stray resources left in the CI account. To mitigate the risk, integration tests should always be contained in a block with an always statement that cleans up if the tests fail. In case that also fails (such as due to a flaky AWS service or broken module) we deploy a lambda function that runs the terminator classes to find and delete stray resources. 15 | 16 | To begin, you need to use the Terminator base class or the DbTerminator base class. We terminate resources found based on their age. Not all AWS resources return the creation date timestamp so those resources are stored in a database with the time when the terminator class located them and we approximate when to delete them from that. 17 | * If the resource has a creation date timestamp use the Terminator base class. 18 | * If the resource does not have a creation date timestamp use the DbTerminator base class. 19 | 20 | Your terminator class requires the following: 21 | * the staticmethod `create` 22 | * the property `name` 23 | * the property `created_time` (only if you are using the Terminator base class) 24 | * the method `terminate` 25 | 26 | You can include the property `id` if there is a unique identifier in addition to a human readable name. 27 | 28 | The `create` method should return the base class `_create` method called with the credentials to create the client, the class name, the boto3 resource name to create the client, and a function for the client to use. The function should list all the given resources for that resource type. 29 | 30 | Here's an example for an EC2 instance terminator class: 31 | 32 | ```python 33 | class Ec2Instance(Terminator): 34 | 35 | @staticmethod 36 | def create(credentials): 37 | 38 | def get_instances(client): 39 | return [i for r in client.describe_instances()['Reservations'] for i in r['Instances']] 40 | 41 | return Terminator._create(credentials, Ec2Instance, 'ec2', get_instances) 42 | ``` 43 | 44 | `self.instance` is an item from the list returned by the base class `_create` method and should be used by the `id`, `name`, and `created_time` properties. 45 | 46 | ```python 47 | @property 48 | def id(self): 49 | return self.instance['InstanceId'] 50 | 51 | @property 52 | def name(self): 53 | return self.instance['PrivateDnsName'] 54 | 55 | @property 56 | def created_time(self): 57 | return self.instance['LaunchTime'] 58 | ``` 59 | 60 | The `terminate` method should use self.client to delete the resource. 61 | 62 | ```python 63 | def terminate(self): 64 | self.client.terminate_instances(InstanceIds=[self.id]) 65 | ``` 66 | 67 | To test the terminator class with your own account you can use the [cleanup.py](https://github.com/mattclay/aws-terminator/blob/master/aws/cleanup.py) script. 68 | 69 | Warning: Always use the --check (or -c) flag and the --target flag to avoid accidentally deleting wanted resources. 70 | It is safest to use `cleanup.py` in an empty/dev account. 71 | 72 | To start using `cleanup.py` you will need to: 73 | * Use Python 3.9 74 | * Modify config.yml to use your own accounts. These can be the same account if you're just using `cleanup.py`. 75 | If you use two separate accounts, `lambda_account_id` is the account of the profile that will assume the IAM role in the `test_account_id`. The `test_account_id` is where the terminator class(es) will locate/remove resources. 76 | * Create a role called `ansible-core-ci-test-dev` that your AWS profile can assume. Give this role the permissions required by the terminator class you are testing. 77 | * Set the environment variable `AWS_PROFILE` with the profile you want to use. 78 | * Run `cleanup.py` using the class name as the target to locate the resources in us-east-1: 79 | 80 | python cleanup.py --stage dev --target Ec2Instance -v -c 81 | cleanup : DEBUG located Ec2Instance: count=2 82 | cleanup : DEBUG ignored Ec2Instance: name=, id=i-0c18f88091e78898e age=0 days, 0:05:32, stale=False 83 | cleanup : DEBUG checked Ec2Instance: name=, id=i-0630e2ba640d7dbf1 age=1 days, 20:49:03, stale=True 84 | 85 | * The class property `age_limit` determines when a resource becomes stale. This is 20 minutes by default. Once a resource is stale, the terminator can delete it. Use check mode (-c or --check) to see what your class would delete without actually removing it. 86 | * Once a resource is stale you can test that it can be cleaned up by removing the check mode flag. 87 | For example, `python cleanup.py --stage dev --target Ec2Instance -v`. 88 | * You can forcibly delete resources that are not stale by using --force (or -f). Be aware that this can also remove resources that do not use the Terminator or DbTerminator base classes. Such unsupported resources will not be cleaned up by the CI account. 89 | 90 | After you have tested that your terminator class can be used by `cleanup.py`, submit your pull request. A core developer will review and deploy your changes as outlined below. 91 | 92 | ## IAM Policy Organization 93 | 94 | ### Structure 95 | 96 | Policy docs are in `aws/policy/{group}.yaml`, arranged by the test group. 97 | 98 | ### Policy Groups 99 | - Application Services: CloudFormation, SQS, SNS, SES 100 | - Application Security: Inspector, WAF, etc 101 | - Data Services: Glacier, Glue, Redshift, RDS, etc 102 | - Compute: Autoscaling, EC2, ELBs, etc 103 | - Networking: VPC, ACLs, route tables, NAT Gateways, IGW/VGW, security groups, etc 104 | - PAAS: ECR, EKR, Lambda, etc 105 | - Security Services: IAM, KMS, STS, etc 106 | - Storage Services: S3, etc 107 | 108 | ### IAM Elements 109 | 110 | Policies should generally use the least permissive Actions, Resouces, and Conditions possible. However, there is also a need to prevent policies from exceeding the AWS maximum 6144 characters per policy and 10 policies per account. To help with this wildcards are generally permitted for `Describe*` and `List*` actions for non-security related services. For example, `ec2:Describe*` is permitted. 111 | 112 | # Deploying to AWS 113 | Deploying to AWS is done using an Ansible playbook, which can be easily run with make using the provided Makefile. 114 | 115 | ## Environment Variables 116 | 117 | The playbook requires the following environment variables to be set: 118 | 119 | - `AWS_REGION` - The recommended region is `us-east-1` as that is where Shippable instances run. 120 | - `STAGE` - This must be either `dev` or `prod`. 121 | 122 | ## Deployment Process 123 | 124 | Initial setup can be handled by an administrator, with further updates to the deployment by a power user. 125 | 126 | ### Administrator 127 | 128 | An administrator can deploy the IAM roles and policies with `make terminator`. 129 | 130 | ### Power User 131 | 132 | A power user can deploy everything else with `make terminator_lambda`. 133 | 134 | This user should have the following AWS Managed Policies applied: 135 | 136 | - IAMReadOnlyAccess 137 | - PowerUserAccess 138 | 139 | ### Steps to update permissions and terminator for AWS pull requests 140 | 141 | Use Python 2 (Python 3 not fully supported yet) 142 | 143 | Modify IAM permissions: 144 | - update the appropriate test policy in the policy directory (policy groups are defined below) 145 | - export AWS_PROFILE=youraworksuser 146 | - assume arn:aws:iam::966509639900:role/administrator 147 | - export the sts credentials and unset AWS_PROFILE 148 | - deploy permissions to dev running `make test_policy STAGE=dev` 149 | 150 | Check the permissions by running the integration tests: 151 | - make sure there is no test/integration/cloud-config-aws.yml (will override CI credentials) 152 | - make sure CI key is in ~/.ansible-core-ci.key 153 | - run tests with `ansible-test integration [module] --remote-stage dev --docker default -v` 154 | 155 | If tests create a resource: 156 | - add a new class for the resource in the corresponding file in the terminator directory (use Terminator base class 157 | if resources have a created time, DbTerminator otherwise) 158 | - test terminator with `python cleanup.py --stage dev -c -v`, make sure modified terminator resource class shows up 159 | in the output 160 | 161 | Make a pull request to CI 162 | 163 | If tests pass and CI PR is ready to merge: 164 | - deploy to prod with `make test_policy STAGE=prod` 165 | 166 | If there are modifications to the terminator: 167 | - after deploying to dev and prod run `make terminator_lambda` using aworks user credentials 168 | -------------------------------------------------------------------------------- /aws/cleanup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # PYTHON_ARGCOMPLETE_OK 3 | """Terminate or destroy stale resources in the AWS test account.""" 4 | 5 | import argparse 6 | import logging 7 | import os 8 | import sys 9 | import yaml 10 | 11 | import boto3 12 | 13 | try: 14 | import argcomplete 15 | except ImportError: 16 | argcomplete = None 17 | 18 | from terminator import ( 19 | cleanup, 20 | get_concrete_subclasses, 21 | logger, 22 | Terminator, 23 | ) 24 | 25 | 26 | def main(): 27 | logger.setLevel(logging.INFO) 28 | 29 | formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s') 30 | 31 | console = logging.StreamHandler() 32 | console.setFormatter(formatter) 33 | 34 | logger.addHandler(console) 35 | 36 | args = parse_args() 37 | 38 | if args.verbose: 39 | logger.setLevel(logging.DEBUG) 40 | 41 | if args.config_file: 42 | config_path = args.config_file 43 | else: 44 | base_path = os.path.dirname(__file__) 45 | config_path = os.path.join(base_path, 'config.yml') 46 | logger.debug('Config path: %s', config_path) 47 | 48 | def default_ctor(_dummy, tag_suffix, node): 49 | return tag_suffix + ' ' + node.value 50 | 51 | yaml.add_multi_constructor('', default_ctor) 52 | 53 | with open(config_path, encoding="utf-8") as config_fd: 54 | config = yaml.load(config_fd, Loader=yaml.BaseLoader) 55 | 56 | test_account_id = config['test_account_id'] 57 | api_name = config['api_name'] 58 | 59 | account_id = boto3.client('sts').get_caller_identity().get('Account') 60 | 61 | if account_id != config['lambda_account_id']: 62 | sys.exit(f'The terminator must be run from the lambda account: {config["lambda_account_id"]}') 63 | 64 | cleanup(args.stage, check=args.check, force=args.force, api_name=api_name, test_account_id=test_account_id, targets=args.target) 65 | 66 | 67 | def parse_args(): 68 | parser = argparse.ArgumentParser(description='Terminate or destroy stale resources in the AWS test account.') 69 | 70 | parser.add_argument('-c', '--check', 71 | action='store_true', 72 | help='do not terminate resources') 73 | 74 | parser.add_argument('-f', '--force', 75 | action='store_true', 76 | help='do not skip unsupported or stale resources') 77 | 78 | parser.add_argument('-v', '--verbose', 79 | action='store_true', 80 | help='increase logging verbosity') 81 | 82 | parser.add_argument('--stage', 83 | choices=['prod', 'dev'], 84 | required=True, 85 | help='stage to use for database and policy access') 86 | 87 | parser.add_argument('--config-file', 88 | metavar='config_file', 89 | help='Where to read the configuration file from') 90 | 91 | parser.add_argument('--target', 92 | choices=sorted([value.__name__ for value in get_concrete_subclasses(Terminator)] + ['Database']), 93 | metavar='target', 94 | action='append', 95 | help='class to run') 96 | 97 | if argcomplete: 98 | argcomplete.autocomplete(parser) 99 | 100 | args = parser.parse_args() 101 | 102 | return args 103 | 104 | 105 | if __name__ == '__main__': 106 | main() 107 | -------------------------------------------------------------------------------- /aws/config.yml: -------------------------------------------------------------------------------- 1 | # This is the account in which the lambda functions are run. 2 | # It must be different from the account which runs the integration tests. 3 | lambda_account_id: '900854079004' 4 | 5 | # This is the account in which the integration tests are run. 6 | # It must be different from the account which runs the lambda functions. 7 | test_account_id: '966509639900' 8 | 9 | # The API name is used to tag and prefix aws resources. 10 | api_name: 'ansible-core-ci' 11 | 12 | # The AWS Region that tests will be run in 13 | aws_region: 'us-east-1' 14 | -------------------------------------------------------------------------------- /aws/deploy-test-policy.yml: -------------------------------------------------------------------------------- 1 | - hosts: localhost 2 | collections: 3 | - mattclay.aws 4 | gather_facts: False 5 | vars: 6 | stage: "{{ lookup('env', 'STAGE') }}" 7 | tasks: 8 | - name: load config 9 | tags: always 10 | include_vars: 11 | file: config.yml 12 | - name: check config 13 | tags: always 14 | assert: 15 | that: 'test_account_id != lambda_account_id' 16 | - name: check required variables 17 | tags: always 18 | fail: "msg='Environment variable {{ item | upper}} must be set.'" 19 | when: not lookup('vars', item) 20 | with_items: 21 | - stage 22 | - name: get aws account facts 23 | tags: always 24 | aws_account_facts: 25 | - name: show configuration 26 | tags: always 27 | debug: msg="stage={{ stage }}, aws_account_id={{ aws_account_id }}" 28 | - name: verify aws_account_id matches test_account_id 29 | tags: always 30 | assert: 31 | that: "aws_account_id == test_account_id" 32 | - name: Create a list of policy file names without the path or extension 33 | tags: iam 34 | set_fact: 35 | policies: '{{ lookup("fileglob", "policy/*.yaml", wantlist=True) | map("basename") | map("regex_findall", "^(.*?)\.yaml") | list | flatten }}' 36 | - name: Check policies do not exceed AWS size limits 37 | assert: 38 | that: 39 | - "{{ lookup('template', 'policy/' + item + '.yaml') | from_yaml | to_json |length < 6144 }}" 40 | fail_msg: "{{ 'policy/' + item + '.yaml'}} exceeds the 6144 max character count for AWS managed policies" 41 | loop: "{{ policies }}" 42 | - name: Create a list of managed policies 43 | tags: iam 44 | set_fact: 45 | managed_policies: '{{ policies | map("regex_replace", "^(.*)$", api_name + "-test" + "-\1-" + stage) | list }}' 46 | - name: create iam managed policy for test permission groups 47 | tags: iam 48 | iam_managed_policy: 49 | policy_name: "{{ api_name }}-test-{{ item }}-{{ stage }}" 50 | state: present 51 | policy_description: "{{ api_name }}-{{ stage }} {{ item }} policy for CI tests" 52 | policy: "{{ lookup('template', 'policy/' + item + '.yaml') | from_yaml | to_json }}" 53 | loop: "{{ policies }}" 54 | - name: create iam role for running integration tests 55 | tags: iam 56 | iam_role: 57 | name: "{{ api_name }}-test-{{ stage }}" 58 | state: present 59 | managed_policies: "{{ managed_policies }}" 60 | create_instance_profile: no 61 | max_session_duration: 7200 62 | assume_role_policy_document: 63 | Version: '2012-10-17' 64 | Statement: 65 | - Action: sts:AssumeRole 66 | Effect: Allow 67 | Principal: 68 | AWS: 'arn:aws:iam::{{ lambda_account_id }}:root' 69 | - name: create iam role for lambda functions created by integration tests 70 | tags: iam 71 | iam_role: 72 | name: "ansible_lambda_role" 73 | state: present 74 | create_instance_profile: no 75 | assume_role_policy_document: 76 | Version: '2012-10-17' 77 | Statement: 78 | - Action: sts:AssumeRole 79 | Effect: Allow 80 | Principal: 81 | Service: lambda.amazonaws.com 82 | -------------------------------------------------------------------------------- /aws/policy/application-security.yaml: -------------------------------------------------------------------------------- 1 | Version: '2012-10-17' 2 | Statement: 3 | 4 | - Sid: AllowRegionalRestrictedResourceActionsWhichIncurFees 5 | Effect: Allow 6 | Action: 7 | - wafv2:AssociateWebACL 8 | - wafv2:DeleteRuleGroup 9 | - wafv2:CreateRuleGroup 10 | - wafv2:PutFirewallManagerRuleGroups 11 | - wafv2:DeleteWebACL 12 | - wafv2:CreateWebACL 13 | - wafv2:CreateIPSet 14 | - wafv2:DeleteIPSet 15 | - wafv2:CheckCapacity 16 | - wafv2:DeleteLoggingConfiguration 17 | - wafv2:PutLoggingConfiguration 18 | - wafv2:DisassociateWebACL 19 | - wafv2:UpdateWebACL 20 | - wafv2:UpdateRuleGroup 21 | - wafv2:DeleteFirewallManagerRuleGroups 22 | - wafv2:DisassociateFirewallManager 23 | - wafv2:UpdateIPSet 24 | Resource: 25 | - 'arn:aws:wafv2:{{ aws_region }}:{{ aws_account_id }}:*' 26 | 27 | - Sid: AllowRegionalUnrestrictedResourceActionsWhichIncurNoFees 28 | Effect: Allow 29 | Action: 30 | - inspector:List* 31 | - inspector:CreateResourceGroup 32 | - inspector:CreateAssessmentTarget 33 | - inspector:Describe* 34 | - inspector:UpdateAssessmentTarget 35 | - inspector:DeleteAssessmentTarget 36 | - inspector:CreateAssessmentTemplate 37 | - inspector:DeleteAssessmentTemplate 38 | - inspector:SetTagsForResource 39 | - waf:CreateByteMatchSet 40 | - waf:CreateGeoMatchSet 41 | - waf:CreateIPSet 42 | - waf:CreateRateBasedRule 43 | - waf:CreateRegexMatchSet 44 | - waf:CreateRegexPatternSet 45 | - waf:CreateRule 46 | - waf:CreateRuleGroup 47 | - waf:CreateSizeConstraintSet 48 | - waf:CreateSqlInjectionMatchSet 49 | - waf:CreateWebACL 50 | - waf:CreateXssMatchSet 51 | - waf:DeleteByteMatchSet 52 | - waf:DeleteGeoMatchSet 53 | - waf:DeleteIPSet 54 | - waf:DeleteRateBasedRule 55 | - waf:DeleteRegexMatchSet 56 | - waf:DeleteRegexPatternSet 57 | - waf:DeleteRule 58 | - waf:DeleteRuleGroup 59 | - waf:DeleteSizeConstraintSet 60 | - waf:DeleteSqlInjectionMatchSet 61 | - waf:DeleteWebACL 62 | - waf:DeleteXssMatchSet 63 | - waf:Get* 64 | - waf:List* 65 | - waf:TagResource 66 | - waf:UntagResource 67 | - waf:UpdateByteMatchSet 68 | - waf:UpdateGeoMatchSet 69 | - waf:UpdateIPSet 70 | - waf:UpdateRateBasedRule 71 | - waf:UpdateRegexMatchSet 72 | - waf:UpdateRegexPatternSet 73 | - waf:UpdateRule 74 | - waf:UpdateSizeConstraintSet 75 | - waf:UpdateSqlInjectionMatchSet 76 | - waf:UpdateWebACL 77 | - waf:UpdateXssMatchSet 78 | - wafv2:Describe* 79 | - wafv2:Get* 80 | - wafv2:List* 81 | - wafv2:TagResource 82 | - wafv2:UntagResource 83 | Resource: "*" 84 | Condition: 85 | StringEquals: 86 | aws:RequestedRegion: 87 | - '{{ aws_region }}' 88 | -------------------------------------------------------------------------------- /aws/policy/application-services.yaml: -------------------------------------------------------------------------------- 1 | Version: '2012-10-17' 2 | Statement: 3 | - Sid: AllowGlobalUnrestrictedResourceActionsWhichIncurNoFees 4 | Effect: Allow 5 | Action: 6 | ### 7 | # These cloudformation permissions simply enable use of the Cloud Control API. 8 | # The underlying resources the API is managing would still require their own permissions. 9 | - cloudformation:CancelResourceRequest 10 | - cloudformation:CreateResource 11 | - cloudformation:DeleteResource 12 | - cloudformation:Describe* 13 | - cloudformation:Get* 14 | - cloudformation:List* 15 | - cloudformation:UpdateResource 16 | ### 17 | - cloudwatch:Describe* 18 | - codebuild:BatchGetProjects 19 | - codebuild:List* 20 | - codecommit:Get* 21 | - codecommit:List* 22 | - codepipeline:Get* 23 | - codepipeline:List* 24 | - ec2messages:AcknowledgeMessage 25 | - ec2messages:DeleteMessage 26 | - ec2messages:FailMessage 27 | - ec2messages:Get* 28 | - ec2messages:SendReply 29 | - events:CreateRule 30 | - events:DeleteRule 31 | - events:Describe* 32 | - events:List* 33 | - events:PutRule 34 | - events:PutTargets 35 | - events:RemoveTargets 36 | - glue:Get* 37 | - kinesis:Describe* 38 | - kinesis:List* 39 | - mq:Describe* 40 | - mq:List* 41 | - ses:CreateReceiptRuleSet 42 | - ses:DeleteIdentity 43 | - ses:DeleteIdentityPolicy 44 | - ses:DeleteReceiptRuleSet 45 | - ses:Describe* 46 | - ses:Get* 47 | - ses:List* 48 | - ses:PutIdentityPolicy 49 | - ses:SetActiveReceiptRuleSet 50 | - ses:SetIdentityDkimEnabled 51 | - ses:SetIdentityFeedbackForwardingEnabled 52 | - ses:SetIdentityHeadersInNotificationsEnabled 53 | - ses:SetIdentityNotificationTopic 54 | - ses:VerifyDomainDkim 55 | - ses:VerifyDomainIdentity 56 | - ses:VerifyEmailIdentity 57 | - sqs:CreateQueue 58 | - sqs:DeleteQueue 59 | - sqs:Get* 60 | - sqs:List* 61 | - sqs:SetQueueAttributes 62 | - sqs:TagQueue 63 | - sqs:UntagQueue 64 | - ssm:AddTagsToResource 65 | - ssm:Describe* 66 | - ssm:Get* 67 | - ssm:List* 68 | - ssm:PutComplianceItems 69 | - ssm:PutConfigurePackageResult 70 | - ssm:PutInventory 71 | - ssm:RemoveTagsFromResource 72 | - ssm:StartSession 73 | - ssm:TerminateSession 74 | - ssm:UpdateAssociationStatus 75 | - ssm:UpdateInstanceAssociationStatus 76 | - ssm:UpdateInstanceInformation 77 | - ssmmessages:CreateControlChannel 78 | - ssmmessages:CreateDataChannel 79 | - ssmmessages:OpenControlChannel 80 | - ssmmessages:OpenDataChannel 81 | - SNS:Get* 82 | - SNS:List* 83 | - states:Describe* 84 | - states:List* 85 | Resource: "*" 86 | 87 | - Sid: AllowGlobalResourceRestrictedActionsWhichIncurNoFees 88 | Effect: Allow 89 | Action: 90 | - ssm:CreateDocument 91 | - ssm:DeleteDocument 92 | - cloudformation:CreateChangeSet 93 | - cloudformation:CreateStack 94 | - cloudformation:DeleteChangeSet 95 | - cloudformation:DeleteStack 96 | - cloudformation:SetStackPolicy 97 | - cloudformation:UpdateStack 98 | - cloudformation:UpdateTerminationProtection 99 | - cloudwatch:DeleteAlarms 100 | - cloudwatch:PutMetricAlarm 101 | - codebuild:CreateProject 102 | - codebuild:DeleteProject 103 | - codebuild:UpdateProject 104 | - codecommit:CreateRepository 105 | - codecommit:DeleteRepository 106 | - codecommit:UpdateRepositoryDescription 107 | - codepipeline:CreatePipeline 108 | - codepipeline:DeletePipeline 109 | - codepipeline:UpdatePipeline 110 | - glue:DeleteCrawler 111 | - glue:DeleteJob 112 | - glue:TagResource 113 | - glue:UntagResource 114 | - glue:UpdateCrawler 115 | - glue:UpdateJob 116 | - kinesis:AddTagsToStream 117 | - kinesis:RemoveTagsFromStream 118 | - kinesis:StartStreamEncryption 119 | - kinesis:StopStreamEncryption 120 | - mq:CreateTags 121 | - SNS:CreateTopic 122 | - SNS:DeleteTopic 123 | - SNS:TagResource 124 | - SNS:SetSubscriptionAttributes 125 | - SNS:SetTopicAttributes 126 | - SNS:Subscribe 127 | - SNS:Unsubscribe 128 | - SNS:UntagResource 129 | - ssm:DeleteParameter 130 | - ssm:PutParameter 131 | - states:DeleteStateMachine 132 | - states:TagResource 133 | - states:UntagResource 134 | Resource: 135 | - 'arn:aws:ssm:{{ aws_region }}:{{ aws_account_id }}:document/*' 136 | - 'arn:aws:cloudformation:{{ aws_region }}:{{ aws_account_id }}:stack/*' 137 | - 'arn:aws:cloudwatch:{{ aws_region }}:{{ aws_account_id }}:alarm:*' 138 | - 'arn:aws:codebuild:{{ aws_region }}:{{ aws_account_id }}:*' 139 | - 'arn:aws:codecommit:{{ aws_region }}:{{ aws_account_id }}:*' 140 | - 'arn:aws:codepipeline:{{ aws_region }}:{{ aws_account_id }}:*' 141 | - 'arn:aws:glue:{{ aws_region }}:{{ aws_account_id }}:crawler/*' 142 | - 'arn:aws:glue:{{ aws_region }}:{{ aws_account_id }}:job/*' 143 | - 'arn:aws:kinesis:{{ aws_region }}:{{ aws_account_id }}:stream/*' 144 | - 'arn:aws:mq:{{ aws_region }}:{{ aws_account_id }}:*' 145 | - 'arn:aws:sns:{{ aws_region }}:{{ aws_account_id }}:*' 146 | - 'arn:aws:ssm:{{ aws_region }}:{{ aws_account_id }}:parameter/*' 147 | - 'arn:aws:ssm:{{ aws_region }}::parameter/aws/service/*' 148 | - 'arn:aws:states:{{ aws_region }}:{{ aws_account_id }}:*' 149 | 150 | - Sid: AllowGlobalRestrictedResourceActionsWhichIncurFees 151 | Effect: Allow 152 | Action: 153 | - glue:CreateCrawler 154 | - glue:CreateJob 155 | - kinesis:CreateStream 156 | - kinesis:DecreaseStreamRetentionPeriod 157 | - kinesis:DeleteStream 158 | - kinesis:IncreaseStreamRetentionPeriod 159 | - kinesis:UpdateShardCount 160 | - mq:CreateBroker 161 | - mq:DeleteBroker 162 | - SNS:Publish 163 | - states:CreateStateMachine 164 | - states:StartExecution 165 | - states:StopExecution 166 | - states:UpdateStateMachine 167 | Resource: 168 | - 'arn:aws:sns:{{ aws_region }}:{{ aws_account_id }}:*' 169 | - 'arn:aws:states:{{ aws_region }}:{{ aws_account_id }}:*' 170 | - 'arn:aws:mq:{{ aws_region }}:{{ aws_account_id }}:*' 171 | - 'arn:aws:kinesis:{{ aws_region }}:{{ aws_account_id }}:stream/*' 172 | - 'arn:aws:glue:{{ aws_region }}:{{ aws_account_id }}:crawler/*' 173 | - 'arn:aws:glue:{{ aws_region }}:{{ aws_account_id }}:job/*' 174 | 175 | # Used to test some of the cross-account features 176 | - Sid: PermitReadOnlyThirdParty 177 | Effect: Allow 178 | Action: 179 | - SNS:Subscribe 180 | - SNS:Unsubscribe 181 | Resource: 182 | # https://aws.amazon.com/blogs/aws/subscribe-to-aws-public-ip-address-changes-via-amazon-sns/ 183 | - 'arn:aws:sns:us-east-1:806199016981:AmazonIpSpaceChanged' 184 | -------------------------------------------------------------------------------- /aws/policy/compute.yaml: -------------------------------------------------------------------------------- 1 | Version: '2012-10-17' 2 | Statement: 3 | 4 | # Restrict the types of instances that can be started 5 | # ASGs call run-instances --dry-run so the actions need to be grouped 6 | - Sid: AllowRunInstancesInstanceType 7 | Effect: Allow 8 | Action: 9 | - autoscaling:AttachInstances 10 | - autoscaling:CreateAutoScalingGroup 11 | - autoscaling:CreateLaunchConfiguration 12 | - autoscaling:UpdateAutoScalingGroup 13 | - autoscaling:DetachInstances 14 | - ec2:RunInstances 15 | - ec2:StartInstances 16 | Resource: 17 | - 'arn:aws:autoscaling:{{ aws_region }}:{{ aws_account_id }}:launchConfiguration:*' 18 | - 'arn:aws:autoscaling:{{ aws_region }}:{{ aws_account_id }}:autoScalingGroup:*' 19 | - 'arn:aws:ec2:{{ aws_region }}:{{ aws_account_id }}:instance/*' 20 | - 'arn:aws:ec2:{{ aws_region }}:{{ aws_account_id }}:image/*' 21 | - 'arn:aws:ec2:{{ aws_region }}:{{ aws_account_id }}:launch-template/*' 22 | - 'arn:aws:ec2:{{ aws_region }}:{{ aws_account_id }}:snapshot/*' 23 | Condition: 24 | StringEqualsIfExists: 25 | ec2:InstanceType: 26 | - t2.nano 27 | - t2.micro 28 | - t3.nano 29 | - t3.micro 30 | - t3a.micro 31 | - m1.large # lowest cost instance type with EBS optimization supported 32 | 33 | # Permit RunInstance to access any of the usual objects attached to an 34 | # instance 35 | - Sid: AllowEc2RunInstances 36 | Effect: Allow 37 | Action: 38 | - ec2:RunInstances 39 | Resource: 40 | - 'arn:aws:ec2:{{ aws_region }}:{{ aws_account_id }}:key-pair/*' 41 | - 'arn:aws:ec2:{{ aws_region }}:{{ aws_account_id }}:network-interface/*' 42 | - 'arn:aws:ec2:{{ aws_region }}:{{ aws_account_id }}:placement-group/*' 43 | - 'arn:aws:ec2:{{ aws_region }}:{{ aws_account_id }}:security-group/*' 44 | - 'arn:aws:ec2:{{ aws_region }}:{{ aws_account_id }}:subnet/*' 45 | - 'arn:aws:ec2:{{ aws_region }}:{{ aws_account_id }}:volume/*' 46 | - 'arn:aws:ec2:{{ aws_region }}::image/*' 47 | - 'arn:aws:ec2:{{ aws_region }}:{{ aws_account_id }}:launch-template/*' 48 | - 'arn:aws:autoscaling:{{ aws_region }}:{{ aws_account_id }}:autoScalingGroup*' 49 | 50 | - Sid: AllowRegionalUnrestrictedResourceActionsWhichIncurNoFees 51 | Effect: Allow 52 | Action: 53 | - ec2:AttachVolume 54 | - ec2:CancelSpotInstanceRequests 55 | - ec2:CreateImage 56 | - ec2:CreateKeyPair 57 | - ec2:CreateLaunchTemplate 58 | - ec2:CreateLaunchTemplateVersion 59 | - ec2:CreatePlacementGroup 60 | - ec2:CreateSnapshot 61 | - ec2:CreateTags 62 | - ec2:DeleteKeyPair 63 | - ec2:DeleteLaunchTemplate 64 | - ec2:DeleteLaunchTemplateVersions 65 | - ec2:DeletePlacementGroup 66 | - ec2:DeleteSnapshot 67 | - ec2:DeleteTags 68 | - ec2:DeregisterImage 69 | - ec2:DetachVolume 70 | - ec2:DisassociateIamInstanceProfile 71 | - ec2:Get* 72 | - ec2:ImportKeyPair 73 | - ec2:ModifyImageAttribute 74 | - ec2:ModifyInstanceAttribute 75 | - ec2:ModifyInstanceMetadataOptions 76 | - ec2:ModifyLaunchTemplate 77 | - ec2:ModifySnapshotAttribute 78 | - ec2:ModifyVolume 79 | - ec2:RegisterImage 80 | - ec2:ReplaceIamInstanceProfileAssociation 81 | - ec2:ResetSnapshotAttribute 82 | - ec2:StopInstances 83 | - ec2:TerminateInstances 84 | Resource: 85 | - "*" 86 | Condition: 87 | StringEquals: 88 | ec2:Region: 89 | - '{{ aws_region }}' 90 | 91 | # Spot Request permissions need to be pretty open 92 | # https://stackoverflow.com/questions/36570812/aws-ec2-iam-policy-for-ec2requestspotinstances 93 | - Sid: AllowGlobalUnrestrictedResourceActionsWhichIncurFees 94 | Effect: Allow 95 | Action: 96 | - ec2:RequestSpotInstances 97 | Resource: 98 | - "*" 99 | Condition: 100 | StringEqualsIfExists: 101 | ec2:InstanceType: 102 | - t2.nano 103 | - t2.micro 104 | - t3.nano 105 | - t3.micro 106 | - t3a.micro 107 | - m1.large # lowest cost instance type with EBS optimization supported 108 | 109 | # ASG and ELB don't like being region restricted. 110 | - Sid: AllowGlobalUnrestrictedResourceActionsWhichIncurNoFees 111 | Effect: Allow 112 | Action: 113 | - autoscaling:Describe* 114 | - autoscaling:ResumeProcesses 115 | - autoscaling:SuspendProcesses 116 | - ec2:Describe* 117 | - elasticloadbalancing:DeleteRule 118 | - elasticloadbalancing:DeleteListener 119 | - elasticloadbalancing:Describe* 120 | - elasticloadbalancing:DeregisterTargets 121 | - elasticloadbalancing:ModifyListener 122 | - elasticloadbalancing:ModifyTargetGroupAttributes 123 | - elasticloadbalancing:ModifyRule 124 | - elasticloadbalancing:SetIpAddressType 125 | - elasticloadbalancing:SetRulePriorities 126 | Resource: 127 | - "*" 128 | 129 | - Sid: AllowGlobalRestrictedResourceActionsWhichIncurFees 130 | Effect: Allow 131 | Action: 132 | - autoscaling:EnableMetricsCollection 133 | - ec2:CreateVolume 134 | - elasticloadbalancing:CreateLoadBalancer* 135 | - elasticloadbalancing:CreateRule 136 | Resource: 137 | - 'arn:aws:ec2:{{ aws_region }}:{{ aws_account_id }}:volume/*' 138 | - 'arn:aws:elasticloadbalancing:{{ aws_region }}:{{ aws_account_id }}:*' 139 | - 'arn:aws:autoscaling:{{ aws_region }}:{{ aws_account_id }}:autoScalingGroup*' 140 | 141 | - Sid: AllowGlobalResourceRestrictedActionsWhichIncurNoFees 142 | Effect: Allow 143 | Action: 144 | - autoscaling:AttachLoadBalancerTargetGroups 145 | - autoscaling:CancelInstanceRefresh 146 | - autoscaling:CompleteLifecycleAction 147 | - autoscaling:CreateOrUpdateTags 148 | - autoscaling:Delete* 149 | - autoscaling:DetachLoadBalancers 150 | - autoscaling:DetachLoadBalancerTargetGroups 151 | - autoscaling:DisableMetricsCollection 152 | - autoscaling:PutScalingPolicy 153 | - autoscaling:PutScheduledUpdateGroupAction 154 | - autoscaling:PutLifecycleHook 155 | - autoscaling:StartInstanceRefresh 156 | - autoscaling:SetInstanceHealth 157 | - autoscaling:SetInstanceProtection 158 | - autoscaling:TerminateInstanceInAutoScalingGroup 159 | - ec2:DeleteVolume 160 | - elasticloadbalancing:AddListenerCertificates 161 | - elasticloadbalancing:AddTags 162 | - elasticloadbalancing:ApplySecurityGroupsToLoadBalancer 163 | - elasticloadbalancing:AttachLoadBalancerToSubnets 164 | - elasticloadbalancing:ConfigureHealthCheck 165 | - elasticloadbalancing:CreateAppCookieStickinessPolicy 166 | - elasticloadbalancing:CreateLBCookieStickinessPolicy 167 | - elasticloadbalancing:CreateListener 168 | - elasticloadbalancing:CreateTargetGroup 169 | - elasticloadbalancing:Delete* 170 | - elasticloadbalancing:DeregisterInstancesFromLoadBalancer 171 | - elasticloadbalancing:DetachLoadBalancerFromSubnets 172 | - elasticloadbalancing:DisableAvailabilityZonesForLoadBalancer 173 | - elasticloadbalancing:EnableAvailabilityZonesForLoadBalancer 174 | - elasticloadbalancing:ModifyLoadBalancerAttributes 175 | - elasticloadbalancing:RemoveTags 176 | - elasticloadbalancing:RegisterInstancesWithLoadBalancer 177 | - elasticloadbalancing:RegisterTargets 178 | - elasticloadbalancing:SetLoadBalancer* 179 | - elasticloadbalancing:SetSecurityGroups 180 | - elasticloadbalancing:SetWebACL 181 | Resource: 182 | - 'arn:aws:autoscaling:{{ aws_region }}:{{ aws_account_id }}:launchConfiguration:*' 183 | - 'arn:aws:autoscaling:{{ aws_region }}:{{ aws_account_id }}:autoScalingGroup:*' 184 | - 'arn:aws:ec2:{{ aws_region }}:{{ aws_account_id }}:volume/*' 185 | - 'arn:aws:elasticfilesystem:{{ aws_region }}:{{ aws_account_id }}:file-system/*' 186 | - 'arn:aws:elasticloadbalancing:{{ aws_region }}:{{ aws_account_id }}:targetgroup/*' 187 | - 'arn:aws:elasticloadbalancing:{{ aws_region }}:{{ aws_account_id }}:loadbalancer/*' 188 | - 'arn:aws:elasticloadbalancing:{{ aws_region }}:{{ aws_account_id }}:listener/*' 189 | -------------------------------------------------------------------------------- /aws/policy/data-services.yaml: -------------------------------------------------------------------------------- 1 | Version: '2012-10-17' 2 | Statement: 3 | - Sid: AllowGlobalUnrestrictedResourceActionsWhichIncurNoFees 4 | Effect: Allow 5 | Action: 6 | - dms:CreateEndpoint 7 | - dms:Describe* 8 | - dms:List* 9 | - dynamodb:Get* 10 | - dynamodb:Describe* 11 | - dynamodb:List* 12 | - dynamodb:Scan 13 | - elasticache:Describe* 14 | - elasticache:List* 15 | - glacier:List* 16 | - glue:Get* 17 | - glue:CreateConnection 18 | - glue:DeleteConnection 19 | - glue:UpdateConnection 20 | - rds:Describe* 21 | - rds:List* 22 | - redshift:Describe* 23 | Resource: "*" 24 | - Sid: AllowGlobalResourceRestrictedActionsWhichIncurNoFees 25 | Effect: Allow 26 | Action: 27 | - dms:AddTagsToResource 28 | - dms:CreateReplicationSubnetGroup 29 | - dms:DeleteEndpoint 30 | - dms:DeleteReplicationSubnetGroup 31 | - dms:ModifyEndpoint 32 | - dms:ModifyReplicationSubnetGroup 33 | - dms:RemoveTagsFromResource 34 | - dynamodb:CreateTable 35 | - dynamodb:DeleteItem 36 | - dynamodb:DeleteTable 37 | - dynamodb:PutItem 38 | - dynamodb:TagResource 39 | - dynamodb:UntagResource 40 | - dynamodb:UpdateContinuousBackups 41 | - dynamodb:UpdateItem 42 | - dynamodb:UpdateTable 43 | - elasticache:AddTagsToResource 44 | - elasticache:CreateCacheSecurityGroup 45 | - elasticache:CreateCacheSubnetGroup 46 | - elasticache:DeleteCacheCluster 47 | - elasticache:DeleteCacheSecurityGroup 48 | - elasticache:DeleteCacheSubnetGroup 49 | - elasticache:ModifyCacheCluster 50 | - elasticache:ModifyCacheParameterGroup 51 | - elasticache:ModifyCacheSubnetGroup 52 | - elasticache:RemoveTagsFromResource 53 | - glacier:CreateVault 54 | - glacier:DeleteVault 55 | - glacier:AddTagsToVault 56 | - glacier:RemoveTagsFromVault 57 | - redshift:CreateClusterSubnetGroup 58 | - redshift:CreateTags 59 | - redshift:DeleteCluster 60 | - redshift:DeleteClusterSubnetGroup 61 | - redshift:DeleteTags 62 | - redshift:ModifyCluster 63 | - redshift:ModifyClusterSubnetGroup 64 | - redshift:RebootCluster 65 | - rds:AddTagsToResource 66 | - rds:CreateDBParameterGroup 67 | - rds:CreateDBClusterParameterGroup 68 | - rds:CreateDBSubnetGroup 69 | - rds:DeleteDBCluster 70 | - rds:DeleteDBParameterGroup 71 | - rds:DeleteDBClusterParameterGroup 72 | - rds:DeleteDBSubnetGroup 73 | - rds:RestoreDBInstanceToPointInTime 74 | - rds:RestoreDBInstanceFromDBSnapshot 75 | - rds:RestoreDBInstanceFromS3 76 | - rds:CreateDBInstanceReadReplica 77 | - rds:CreateDBInstance 78 | - rds:ModifyDBInstance 79 | - rds:DeleteDBInstance 80 | - rds:StopDBCluster 81 | - rds:StopDBInstance 82 | - rds:StartDBCluster 83 | - rds:StartDBInstance 84 | - rds:PromoteReadReplica 85 | - rds:RebootDBCluster 86 | - rds:RebootDBInstance 87 | - rds:ModifyDBCluster 88 | - rds:ModifyDBParameterGroup 89 | - rds:ModifyDBClusterParameterGroup 90 | - rds:ModifyDBSubnetGroup 91 | - rds:RemoveTagsFromResource 92 | - rds:CreateOptionGroup 93 | - rds:ModifyOptionGroup 94 | - rds:DeleteOptionGroup 95 | - rds:CreateDBClusterSnapshot 96 | - rds:DeleteDBClusterSnapshot 97 | - rds:CreateDBSnapshot 98 | - rds:DeleteDBSnapshot 99 | - rds:CopyDBSnapshot 100 | - rds:StartExportTask 101 | - rds:CancelExportTask 102 | - rds:RestoreDBClusterToPointInTime 103 | - rds:RestoreDBClusterFromSnapshot 104 | - rds:RestoreDBClusterFromS3 105 | - rds:PromoteReadReplicaDBCluster 106 | - rds:CopyDBClusterSnapshot 107 | Resource: 108 | - 'arn:aws:dms:{{ aws_region }}:{{ aws_account_id }}:endpoint:*' 109 | - 'arn:aws:dms:{{ aws_region }}:{{ aws_account_id }}:subgrp:*' 110 | - 'arn:aws:dynamodb:{{ aws_region }}:{{ aws_account_id }}:table/*' 111 | - 'arn:aws:elasticache:{{ aws_region }}:{{ aws_account_id }}:cluster:*' 112 | - 'arn:aws:elasticache:{{ aws_region }}:{{ aws_account_id }}:subnetgroup:*' 113 | - 'arn:aws:elasticache:{{ aws_region }}:{{ aws_account_id }}:parametergroup:*' 114 | - 'arn:aws:elasticache:{{ aws_region }}:{{ aws_account_id }}:securitygroup:*' 115 | - 'arn:aws:glacier:{{ aws_region }}:{{ aws_account_id }}:vaults/*' 116 | - 'arn:aws:redshift:{{ aws_region }}:{{ aws_account_id }}:cluster:*' 117 | - 'arn:aws:redshift:{{ aws_region }}:{{ aws_account_id }}:subnetgroup:*' 118 | - 'arn:aws:rds:{{ aws_region }}:{{ aws_account_id }}:subgrp:*' 119 | - 'arn:aws:rds:{{ aws_region }}:{{ aws_account_id }}:cluster:*' 120 | - 'arn:aws:rds:{{ aws_region }}:{{ aws_account_id }}:db:*' 121 | - 'arn:aws:rds:{{ aws_region }}:{{ aws_account_id }}:pg:*' 122 | - 'arn:aws:rds:{{ aws_region }}:{{ aws_account_id }}:cluster-pg:*' 123 | - 'arn:aws:rds:{{ aws_region }}:{{ aws_account_id }}:og:*' 124 | - 'arn:aws:dms:{{ aws_region }}:{{ aws_account_id }}:endpoint:*' 125 | - 'arn:aws:rds:{{ aws_region }}:{{ aws_account_id }}:snapshot:*' 126 | - 'arn:aws:rds:{{ aws_region }}:{{ aws_account_id }}:cluster-snapshot:*' 127 | - Sid: AllowGlobalRestrictedResourceActionsWhichIncurFees 128 | Effect: Allow 129 | Action: 130 | - rds:CreateDBCluster 131 | - elasticache:CreateCacheCluster 132 | - redshift:CreateCluster 133 | Resource: 134 | - 'arn:aws:rds:{{ aws_region }}:{{ aws_account_id }}:cluster:*' 135 | - 'arn:aws:rds:{{ aws_region }}:{{ aws_account_id }}:subgrp:*' 136 | - 'arn:aws:elasticache:{{ aws_region }}:{{ aws_account_id }}:cluster:*' 137 | - 'arn:aws:elasticache:{{ aws_region }}:{{ aws_account_id }}:subnetgroup:*' 138 | - 'arn:aws:elasticache:{{ aws_region }}:{{ aws_account_id }}:parametergroup:*' 139 | - 'arn:aws:elasticache:{{ aws_region }}:{{ aws_account_id }}:securitygroup:*' 140 | - 'arn:aws:redshift:{{ aws_region }}:{{ aws_account_id }}:cluster:*' 141 | # This allows AWS Services to autmatically create their Default Service Linked Roles 142 | # These have fixed policies and can only be assumed by the service itself. 143 | - Sid: AllowServiceLinkedRoleCreation 144 | Effect: Allow 145 | Action: 146 | - iam:CreateServiceLinkedRole 147 | Resource: 148 | - 'arn:aws:iam::{{ aws_account_id }}:role/aws-service-role/elasticache.amazonaws.com/AWSServiceRoleForElastiCache' 149 | - 'arn:aws:iam::{{ aws_account_id }}:role/aws-service-role/kafka.amazonaws.com/AWSServiceRoleForKafka' 150 | Condition: 151 | ForAnyValue:StringEquals: 152 | iam:AWSServiceName: 153 | - 'elasticache.amazonaws.com' 154 | - 'kafka.amazonaws.com' 155 | - Sid: KafkaCluster 156 | Effect: Allow 157 | Action: 158 | - kafka:CreateCluster 159 | - kafka:CreateConfiguration 160 | - kafka:DeleteCluster 161 | - kafka:DeleteConfiguration 162 | - kafka:Describe* 163 | - kafka:Get* 164 | - kafka:List* 165 | - kafka:TagResource 166 | - kafka:RebootBroker 167 | - kafka:UntagResource 168 | - kafka:UpdateBrokerCount 169 | - kafka:UpdateBrokerStorage 170 | - kafka:UpdateBrokerType 171 | - kafka:UpdateClusterConfiguration 172 | - kafka:UpdateClusterKafkaVersion 173 | - kafka:UpdateConfiguration 174 | - kafka:UpdateMonitoring 175 | Resource: "*" 176 | -------------------------------------------------------------------------------- /aws/policy/networking.yaml: -------------------------------------------------------------------------------- 1 | Version: '2012-10-17' 2 | Statement: 3 | - Sid: AllowGlobalUnrestrictedResourceActionsWhichIncurNoFees 4 | Effect: Allow 5 | Action: 6 | - route53:ActivateKeySigningKey 7 | - route53:ChangeResourceRecordSets 8 | - route53:List* 9 | - route53:CreateHostedZone 10 | - route53:CreateKeySigningKey 11 | - route53:Get* 12 | - route53:DeleteHostedZone 13 | - route53:UpdateHostedZoneComment 14 | - route53:AssociateVPCWithHostedZone 15 | - route53:ChangeTagsForResource 16 | - route53:CreateHealthCheck 17 | - route53:DeactivateKeySigningKey 18 | - route53:DeleteHealthCheck 19 | - route53:DeleteKeySigningKey 20 | - route53:DisableHostedZoneDNSSEC 21 | - route53:EnableHostedZoneDNSSEC 22 | - route53:UpdateHealthCheck 23 | - network-firewall:List* 24 | - network-firewall:Describe* 25 | Resource: "*" 26 | 27 | - Sid: AllowRegionalUnrestrictedResourceActionsWhichIncurNoFees 28 | Effect: Allow 29 | Action: 30 | - ec2:AcceptTransitGatewayPeeringAttachment 31 | - ec2:AcceptTransitGatewayVpcAttachment 32 | - ec2:AcceptVpcPeeringConnection 33 | - ec2:AllocateAddress 34 | - ec2:AssociateAddress 35 | - ec2:AssignPrivateIpAddresses 36 | - ec2:UnassignPrivateIpAddresses 37 | - ec2:AssociateDhcpOptions 38 | - ec2:AssociateRouteTable 39 | - ec2:AssociateSubnetCidrBlock 40 | - ec2:AssociateVpcCidrBlock 41 | - ec2:AttachInternetGateway 42 | - ec2:AttachNetworkInterface 43 | - ec2:AttachVpnGateway 44 | - ec2:AuthorizeSecurityGroupEgress 45 | - ec2:AuthorizeSecurityGroupIngress 46 | - ec2:CreateCustomerGateway 47 | - ec2:CreateDhcpOptions 48 | - ec2:CreateEgressOnlyInternetGateway 49 | - ec2:CreateInternetGateway 50 | - ec2:CreateNatGateway 51 | - ec2:CreateTransitGateway 52 | - ec2:CreateTransitGatewayPeeringAttachment 53 | - ec2:CreateTransitGatewayVpcAttachment 54 | - ec2:CreateNetworkAcl 55 | - ec2:CreateNetworkAclEntry 56 | - ec2:CreateNetworkInterface 57 | - ec2:CreateNetworkInterfacePermission 58 | - ec2:CreateRoute 59 | - ec2:CreateRouteTable 60 | - ec2:CreateSecurityGroup 61 | - ec2:CreateSubnet 62 | - ec2:CreateVpc 63 | - ec2:CreateVpcEndpoint 64 | - ec2:CreateVpcPeeringConnection 65 | - ec2:CreateVpnConnection 66 | - ec2:CreateVpnGateway 67 | - ec2:DeleteCustomerGateway 68 | - ec2:DeleteDhcpOptions 69 | - ec2:DeleteEgressOnlyInternetGateway 70 | - ec2:DeleteInternetGateway 71 | - ec2:DeleteNatGateway 72 | - ec2:DeleteNetworkAcl 73 | - ec2:DeleteNetworkAclEntry 74 | - ec2:DeleteNetworkInterface 75 | - ec2:DeleteNetworkInterfacePermission 76 | - ec2:DeleteRoute 77 | - ec2:DeleteRouteTable 78 | - ec2:DeleteSecurityGroup 79 | - ec2:DeleteSubnet 80 | - ec2:DeleteVpc 81 | - ec2:DeleteVpcEndpoints 82 | - ec2:DeleteVpcPeeringConnection 83 | - ec2:DeleteVpnConnection 84 | - ec2:DeleteVpnGateway 85 | - ec2:DeleteTransitGateway 86 | - ec2:DeleteTransitGatewayPeeringAttachment 87 | - ec2:DeleteTransitGatewayVpcAttachment 88 | - ec2:DetachInternetGateway 89 | - ec2:DetachNetworkInterface 90 | - ec2:DetachVpnGateway 91 | - ec2:DisassociateAddress 92 | - ec2:DisassociateRouteTable 93 | - ec2:DisassociateSubnetCidrBlock 94 | - ec2:DisassociateVpcCidrBlock 95 | - ec2:ModifyNetworkInterfaceAttribute 96 | - ec2:ModifySubnetAttribute 97 | - ec2:ModifyTransitGatewayPeeringAttachment 98 | - ec2:ModifyTransitGatewayVpcAttachment 99 | - ec2:ModifyVpcAttribute 100 | - ec2:ModifyVpcEndpoint 101 | - ec2:RejectTransitGatewayPeeringAttachment 102 | - ec2:RejectTransitGatewayVpcAttachment 103 | - ec2:RejectVpcPeeringConnection 104 | - ec2:ReleaseAddress 105 | - ec2:ReplaceNetworkAclAssociation 106 | - ec2:ReplaceNetworkAclEntry 107 | - ec2:ReplaceRoute 108 | # - ec2:ReplaceRouteTableAssociation 109 | - ec2:RevokeSecurityGroupEgress 110 | - ec2:RevokeSecurityGroupIngress 111 | - ec2:UpdateSecurityGroupRuleDescriptionsEgress 112 | - ec2:UpdateSecurityGroupRuleDescriptionsIngress 113 | Resource: 114 | - "*" 115 | Condition: 116 | StringEquals: 117 | ec2:Region: 118 | - '{{ aws_region }}' 119 | 120 | - Sid: AllowRegionalRestrictedResourceActionsWhichIncurNoFees 121 | Effect: Allow 122 | Action: 123 | - network-firewall:AssociateFirewallPolicy 124 | - network-firewall:CreateFirewallPolicy 125 | - network-firewall:CreateRuleGroup 126 | - network-firewall:DeleteFirewallPolicy 127 | - network-firewall:DeleteResourcePolicy 128 | - network-firewall:DeleteRuleGroup 129 | - network-firewall:PutResourcePolicy 130 | - network-firewall:TagResource 131 | - network-firewall:UntagResource 132 | - network-firewall:UpdateFirewallDeleteProtection 133 | - network-firewall:UpdateFirewallDescription 134 | - network-firewall:UpdateFirewallPolicy 135 | - network-firewall:UpdateFirewallPolicyChangeProtection 136 | - network-firewall:UpdateLoggingConfiguration 137 | - network-firewall:UpdateRuleGroup 138 | - network-firewall:UpdateSubnetChangeProtection 139 | Resource: 140 | - 'arn:aws:network-firewall:{{ aws_region }}:{{ aws_account_id }}:firewall/AnsibleTest-*' 141 | - 'arn:aws:network-firewall:{{ aws_region }}:{{ aws_account_id }}:firewall-policy/AnsibleTest-*' 142 | - 'arn:aws:network-firewall:{{ aws_region }}:{{ aws_account_id }}:stateful-rulegroup/AnsibleTest-*' 143 | - 'arn:aws:network-firewall:{{ aws_region }}:{{ aws_account_id }}:stateless-rulegroup/AnsibleTest-*' 144 | 145 | - Sid: AllowRegionalRestrictedResourceActionsWhichIncurFees 146 | Effect: Allow 147 | Action: 148 | - network-firewall:AssociateSubnets 149 | - network-firewall:CreateFirewall 150 | - network-firewall:DeleteFirewall 151 | - network-firewall:DisassociateSubnets 152 | Resource: 153 | - 'arn:aws:network-firewall:{{ aws_region }}:{{ aws_account_id }}:firewall/AnsibleTest-*' 154 | 155 | - Sid: AllowGlobalResourceRestrictedActionsWhichIncurNoFees 156 | Effect: Allow 157 | Action: 158 | - apigateway:DELETE 159 | - apigateway:GET 160 | - apigateway:PATCH 161 | - apigateway:POST 162 | - apigateway:PUT 163 | Resource: 164 | - 'arn:aws:apigateway:{{ aws_region }}::/restapis*' 165 | - 'arn:aws:apigateway:{{ aws_region }}::/tags/*' 166 | -------------------------------------------------------------------------------- /aws/policy/paas.yaml: -------------------------------------------------------------------------------- 1 | Version: '2012-10-17' 2 | Statement: 3 | 4 | - Sid: AllowResourceRestrictedActionsWhichIncurFees 5 | Effect: Allow 6 | Action: 7 | - eks:CreateCluster 8 | - eks:CreateNodegroup 9 | - eks:UpdateNodegroupConfig 10 | - lambda:InvokeFunction 11 | - lightsail:CreateInstances 12 | - lightsail:StartInstance 13 | - lightsail:CreateInstanceSnapshot 14 | - ecr:CompleteLayerUpload 15 | - ecr:InitiateLayerUpload 16 | - ecr:PutImage 17 | - ecr:UploadLayerPart 18 | Resource: 19 | - 'arn:aws:eks:{{ aws_region }}:{{ aws_account_id }}:cluster/*' 20 | - 'arn:aws:eks:{{ aws_region }}:{{ aws_account_id }}:nodegroup/*/*/*' 21 | - 'arn:aws:lambda:{{ aws_region }}:{{ aws_account_id }}:function:*' 22 | - 'arn:aws:lightsail:{{ aws_region }}:{{ aws_account_id }}:*' 23 | - 'arn:aws:ecr:{{ aws_region }}:{{ aws_account_id }}:repository/*' 24 | 25 | - Sid: AllowResourceRestrictedActionsWhichIncurNoFees 26 | Effect: Allow 27 | Action: 28 | - cloudfront:CreateCachePolicy 29 | - cloudfront:CreateInvalidation 30 | - cloudfront:CreateOriginRequestPolicy 31 | - cloudfront:DeleteCloudFrontOriginAccessIdentity 32 | - cloudfront:DeleteCachePolicy 33 | - cloudfront:DeleteDistribution 34 | - cloudfront:DeleteOriginRequestPolicy 35 | - cloudfront:DeleteStreamingDistribution 36 | - cloudfront:TagResource 37 | - cloudfront:UntagResource 38 | - cloudfront:UpdateCachePolicy 39 | - cloudfront:UpdateCloudFrontOriginAccessIdentity 40 | - cloudfront:UpdateDistribution 41 | - cloudfront:UpdateOriginRequestPolicy 42 | - ecr:DeleteLifecyclePolicy 43 | - ecr:DeleteRepository 44 | - ecr:DeleteRepositoryPolicy 45 | - ecr:GetLifecyclePolicy 46 | - ecr:GetRepositoryPolicy 47 | - ecr:PutImageScanningConfiguration 48 | - ecr:PutLifecyclePolicy 49 | - ecr:SetRepositoryPolicy 50 | - ecr:BatchDeleteImages 51 | - ecr:BatchCheckLayerAvailability 52 | - eks:DeleteCluster 53 | - eks:Describe* 54 | - eks:List* 55 | - eks:CreateFargateProfile 56 | - eks:DeleteFargateProfile 57 | - eks:TagResource 58 | - eks:UntagResource 59 | - eks:DescribeNodegroup 60 | - eks:UpdateNodegroupVersion 61 | - eks:DeleteNodegroup 62 | - elasticbeanstalk:CreateApplication 63 | - elasticbeanstalk:DeleteApplication 64 | - elasticbeanstalk:Describe* 65 | - elasticbeanstalk:UpdateApplication 66 | - lambda:AddPermission 67 | - lambda:CreateAlias 68 | - lambda:CreateFunction 69 | - lambda:DeleteAlias 70 | - lambda:DeleteFunction 71 | - lambda:DeleteLayerVersion 72 | - lambda:GetAlias 73 | - lambda:GetFunction 74 | - lambda:GetFunctionConfiguration 75 | - lambda:GetLayerVersion 76 | - lambda:GetPolicy 77 | - lambda:ListLayerVersions 78 | - lambda:ListTags 79 | - lambda:PublishLayerVersion 80 | - lambda:RemovePermission 81 | - lambda:TagResource 82 | - lambda:UntagResource 83 | - lambda:UpdateAlias 84 | - lambda:UpdateFunctionCode 85 | - lambda:UpdateFunctionConfiguration 86 | - lightsail:AllocateStaticIp 87 | - lightsail:CreateKeyPair 88 | - lightsail:DeleteInstance 89 | - lightsail:DeleteKeyPair 90 | - lightsail:GetInstance 91 | - lightsail:GetInstances 92 | - lightsail:GetKeyPairs 93 | - lightsail:GetStaticIp 94 | - lightsail:GetStaticIps 95 | - lightsail:RebootInstance 96 | - lightsail:StopInstance 97 | - lightsail:ReleaseStaticIp 98 | - lightsail:PutInstancePublicPorts 99 | - lightsail:GetInstanceSnapshot 100 | - lightsail:DeleteInstanceSnapshot 101 | - lightsail:GetInstanceSnapshots 102 | Resource: 103 | - 'arn:aws:cloudfront::{{ aws_account_id }}:cache-policy/*' 104 | - 'arn:aws:cloudfront::{{ aws_account_id }}:distribution/*' 105 | - 'arn:aws:cloudfront::{{ aws_account_id }}:origin-access-identity/*' 106 | - 'arn:aws:cloudfront::{{ aws_account_id }}:origin-request-policy/*' 107 | - 'arn:aws:cloudfront::{{ aws_account_id }}:streaming-distribution/*' 108 | - 'arn:aws:ecr:{{ aws_region }}:{{ aws_account_id }}:repository/*' 109 | - 'arn:aws:eks:{{ aws_region }}:{{ aws_account_id }}:cluster/*' 110 | - 'arn:aws:eks:{{ aws_region }}:{{ aws_account_id }}:fargateprofile/*/*/*' 111 | - 'arn:aws:eks:{{ aws_region }}:{{ aws_account_id }}:nodegroup/*/*/*' 112 | - 'arn:aws:elasticbeanstalk:{{ aws_region }}:{{ aws_account_id }}:application/*' 113 | - 'arn:aws:lambda:{{ aws_region }}:{{ aws_account_id }}:function:*' 114 | - 'arn:aws:lightsail:{{ aws_region }}:{{ aws_account_id }}:*' 115 | - 'arn:aws:lambda:{{ aws_region }}:{{ aws_account_id }}:layer:*' 116 | 117 | - Sid: AllowUnrestrictedResourceActionsWhichIncurFees 118 | Effect: Allow 119 | Action: 120 | - cloudfront:CreateDistribution 121 | - cloudfront:CreateStreamingDistribution 122 | - cloudfront:CreateStreamingDistributionWithTags 123 | Resource: 124 | - "*" 125 | 126 | - Sid: AllowUnrestrictedResourceActionsWhichIncurNoFees 127 | Effect: Allow 128 | Action: 129 | - cloudfront:CreateCloudFrontOriginAccessIdentity 130 | - cloudfront:Get* 131 | - cloudfront:List* 132 | - ecr:GetAuthorizationToken 133 | - ecr:CreateRepository 134 | - ecr:Describe* 135 | - ecr:List* 136 | - ecr:PutImageTagMutability 137 | - lambda:GetEventSourceMapping 138 | - lambda:ListAliases 139 | - lambda:ListEventSourceMappings 140 | - lambda:ListFunctions 141 | - lambda:ListLayers 142 | - lambda:ListVersionsByFunction 143 | Resource: 144 | - "*" 145 | 146 | - Sid: AllowLambdaEventSourceMappings 147 | Effect: Allow 148 | Action: 149 | - lambda:CreateEventSourceMapping 150 | - lambda:UpdateEventSourceMapping 151 | - lambda:DeleteEventSourceMapping 152 | Resource: 153 | - "*" 154 | Condition: 155 | StringLike: 156 | lambda:FunctionArn: 157 | - arn:aws:lambda:{{ aws_region }}:{{ aws_account_id }}:function:* 158 | 159 | - Sid: AllowGlobalUnrestrictedResourceActionsWhichIncurFees 160 | Effect: Allow 161 | Action: 162 | - ecs:CreateCluster 163 | Resource: "*" 164 | 165 | - Sid: AllowGlobalUnrestrictedResourceActionsWhichIncurNoFees 166 | Effect: Allow 167 | Action: 168 | - ecs:Describe* 169 | - ecs:List* 170 | - ecs:TagResource 171 | - ecs:UntagResource 172 | - ecs:PutAccountSetting 173 | - ecs:RegisterTaskDefinition 174 | - ecs:DeregisterTaskDefinition 175 | Resource: 176 | - "*" 177 | 178 | - Sid: AllowGlobalRestrictedResourceActionsWhichIncurFees 179 | Effect: Allow 180 | Action: 181 | - ecs:RunTask 182 | - ecs:StartTask 183 | - ecs:StopTask 184 | - ecs:DeleteCluster 185 | - ecs:CreateService 186 | - ecs:DeleteService 187 | - ecs:UpdateService 188 | - ecs:UpdateCluster 189 | - ecs:*CapacityProvider 190 | - ecs:PutClusterCapacityProviders 191 | Resource: 192 | - 'arn:aws:ecs:{{ aws_region }}:{{ aws_account_id }}:*' 193 | -------------------------------------------------------------------------------- /aws/policy/security-services.yaml: -------------------------------------------------------------------------------- 1 | Version: '2012-10-17' 2 | Statement: 3 | 4 | - Sid: AllowAssumeRoleTestsAttachAndDetachPolicy 5 | Effect: Allow 6 | Action: 7 | - iam:AttachRolePolicy 8 | - iam:DetachRolePolicy 9 | Resource: 10 | - 'arn:aws:iam::{{ aws_account_id }}:role/ansible-test-*' 11 | # This is hard coded into DMS... 12 | - 'arn:aws:iam::{{ aws_account_id }}:role/dms-vpc-role' 13 | Condition: 14 | ArnLike: 15 | iam:PolicyArn: 16 | - 'arn:aws:iam::aws:policy/AWSDenyAll' 17 | - 'arn:aws:iam::aws:policy/AmazonEKSServicePolicy' 18 | - 'arn:aws:iam::aws:policy/AmazonEKSClusterPolicy' 19 | - 'arn:aws:iam::aws:policy/AmazonEKSFargatePodExecutionRolePolicy' 20 | - 'arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy' 21 | - 'arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy' 22 | - 'arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly' 23 | - 'arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess' 24 | - 'arn:aws:iam::aws:policy/IAMReadOnlyAccess' 25 | - 'arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore' 26 | - 'arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceRole' 27 | - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' 28 | - 'arn:aws:iam::aws:policy/service-role/AWSLambdaDynamoDBExecutionRole' 29 | - 'arn:aws:iam::aws:policy/AWSLambdaInvocation-DynamoDB' 30 | - 'arn:aws:iam::aws:policy/service-role/AWSLambdaSQSQueueExecutionRole' 31 | - 'arn:aws:iam::aws:policy/service-role/AmazonDMSVPCManagementRole' 32 | - 'arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole' 33 | - 'arn:aws:iam::aws:policy/service-role/AWSServiceRoleForVPCTransitGateway' 34 | - 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy' 35 | - 'arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup' 36 | - 'arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForRestores' 37 | - 'arn:aws:iam::aws:policy/AWSBackupServiceRolePolicyForS3Backup' 38 | - 'arn:aws:iam::aws:policy/AWSBackupServiceRolePolicyForS3Restore' 39 | 40 | - Sid: AllowRegionalUnrestrictedResourceActionsWhichIncurNoFees 41 | Effect: Allow 42 | Action: 43 | - iam:GetUser 44 | - acm:List* 45 | Resource: "*" 46 | Condition: 47 | StringEquals: 48 | aws:RequestedRegion: 49 | - '{{ aws_region }}' 50 | 51 | - Sid: AllowRegionalUnrestrictedResourceActionsWhichIncurFees 52 | Effect: Allow 53 | Action: 54 | - kms:CancelKeyDeletion 55 | - kms:CreateKey 56 | - kms:GenerateDataKey 57 | - kms:GenerateRandom 58 | Resource: "*" 59 | Condition: 60 | StringEquals: 61 | aws:RequestedRegion: 62 | - '{{ aws_region }}' 63 | 64 | - Sid: AllowGlobalUnrestrictedResourceActionsWhichIncurNoFees 65 | Effect: Allow 66 | Action: 67 | - access-analyzer:ValidatePolicy 68 | - iam:GetRole 69 | - iam:List* 70 | - iam:Tag* 71 | - iam:Untag* 72 | - kms:CreateAlias 73 | - kms:CreateGrant 74 | - kms:DeleteAlias 75 | - kms:Describe* 76 | - kms:Disable* 77 | - kms:EnableKey* 78 | - kms:Get* 79 | - kms:List* 80 | - kms:PutKeyPolicy 81 | - kms:RetireGrant 82 | - kms:ScheduleKeyDeletion 83 | - kms:Sign 84 | - kms:TagResource 85 | - kms:UntagResource 86 | - kms:UpdateGrant 87 | - kms:UpdateKeyDescription 88 | - kms:Verify 89 | - logs:List* 90 | - secretsmanager:Describe* 91 | - secretsmanager:GetRandomPassword 92 | - secretsmanager:List* 93 | Resource: "*" 94 | 95 | - Sid: AllowGlobalRestrictedResourceActionsWhichIncurFees 96 | Effect: Allow 97 | Action: 98 | - iam:DeleteServerCertificate 99 | - iam:UploadServerCertificate 100 | - secretsmanager:CreateSecret 101 | - secretsmanager:DeleteSecret 102 | - secretsmanager:Get* 103 | - secretsmanager:RotateSecret 104 | - secretsmanager:TagResource 105 | - secretsmanager:UntagResource 106 | - secretsmanager:UpdateSecret 107 | - secretsmanager:PutResourcePolicy 108 | - secretsmanager:DeleteResourcePolicy 109 | - secretsmanager:RemoveRegionsFromReplication 110 | Resource: 111 | - 'arn:aws:iam::{{ aws_account_id }}:server-certificate/ansible-test-*' 112 | - 'arn:aws:secretsmanager:{{ aws_region }}:{{ aws_account_id }}:secret:ansible-test*' 113 | 114 | - Sid: AllowResourceRestrictedActionsWhichIncurNoFees 115 | Effect: Allow 116 | Action: 117 | - acm:AddTagsToCertificate 118 | - acm:DeleteCertificate 119 | - acm:Describe* 120 | - acm:Get* 121 | - acm:ImportCertificate 122 | - acm:RemoveTagsFromCertificate 123 | - acm:RenewCertificate 124 | - acm:RequestCertificate 125 | - iam:AddRoleToInstanceProfile 126 | - iam:CreateInstanceProfile 127 | - iam:CreateRole 128 | - iam:CreateSAMLProvider 129 | - iam:DeleteInstanceProfile 130 | - iam:DeleteRole 131 | - iam:DeleteSAMLProvider 132 | - iam:GetInstanceProfile 133 | - iam:GetSAMLProvider 134 | - iam:GetServerCertificate 135 | - iam:PassRole 136 | - iam:RemoveRoleFromInstanceProfile 137 | - iam:UpdateSAMLProvider 138 | - iam:UpdateServerCertificate 139 | - kms:Decrypt 140 | - logs:AssociateKmsKey 141 | - logs:CreateLogGroup 142 | - logs:DeleteLogGroup 143 | - logs:DeleteMetricFilter 144 | - logs:Describe* 145 | - logs:DisassociateKmsKey 146 | - logs:PutMetricFilter 147 | - logs:PutRetentionPolicy 148 | - logs:Tag* 149 | - logs:Untag* 150 | - sts:AssumeRole 151 | Resource: 152 | - 'arn:aws:acm:{{ aws_region }}:{{ aws_account_id }}:certificate/*' 153 | - 'arn:aws:iam::{{ aws_account_id }}:instance-profile/ansible-test-*' 154 | - 'arn:aws:iam::{{ aws_account_id }}:saml-provider/ansible-test-*' 155 | - 'arn:aws:iam::{{ aws_account_id }}:server-certificate/ansible-test-*' 156 | - 'arn:aws:iam::{{ aws_account_id }}:role/ansible-test-*' 157 | # dms-vpc-role is hard coded into DMS... 158 | - 'arn:aws:iam::{{ aws_account_id }}:role/dms-vpc-role' 159 | - 'arn:aws:iam::{{ aws_account_id }}:role/rds_export_task' 160 | - 'arn:aws:kms:{{ aws_region }}:{{ aws_account_id }}:key/ansible-test-*' 161 | - 'arn:aws:logs:{{ aws_region }}:{{ aws_account_id }}:log-group:*' 162 | - 'arn:aws:logs:{{ aws_region }}:{{ aws_account_id }}:log-group:ansible-test*' 163 | - 'arn:aws:iam::{{ aws_account_id }}:role/aws-service-role/ecs.amazonaws.com/AWSServiceRoleForECS' 164 | 165 | # This allows AWS Services to autmatically create their Default Service Linked Roles 166 | # These have fixed policies and can only be assumed by the service itself. 167 | - Sid: AllowServiceLinkedRoleCreation 168 | Effect: Allow 169 | Action: 170 | - iam:CreateServiceLinkedRole 171 | Resource: 172 | - 'arn:aws:iam::{{ aws_account_id }}:role/aws-service-role/autoscaling.amazonaws.com/*' 173 | - 'arn:aws:iam::{{ aws_account_id }}:role/aws-service-role/spot.amazonaws.com/*' 174 | - 'arn:aws:iam::{{ aws_account_id }}:role/aws-service-role/eks-fargate.amazonaws.com/*' 175 | - 'arn:aws:iam::{{ aws_account_id }}:role/aws-service-role/eks-nodegroup.amazonaws.com/*' 176 | - 'arn:aws:iam::{{ aws_account_id }}:role/aws-service-role/transitgateway.amazonaws.com/*' 177 | - 'arn:aws:iam::{{ aws_account_id }}:role/aws-service-role/network-firewall.amazonaws.com/*' 178 | - 'arn:aws:iam::{{ aws_account_id }}:role/aws-service-role/ecs.amazonaws.com/*' 179 | - 'arn:aws:iam::{{ aws_account_id }}:role/aws-service-role/memorydb.amazonaws.com/*' 180 | Condition: 181 | ForAnyValue:StringEquals: 182 | iam:AWSServiceName: 183 | - 'autoscaling.amazonaws.com' 184 | - 'spot.amazonaws.com' 185 | - 'eks-fargate.amazonaws.com' 186 | - 'eks-nodegroup.amazonaws.com' 187 | - 'transitgateway.amazonaws.com' 188 | - 'network-firewall.amazonaws.com' 189 | - 'ecs.amazonaws.com' 190 | - 'ecs-test.amazonaws.com' 191 | - 'memorydb.amazonaws.com' 192 | -------------------------------------------------------------------------------- /aws/policy/storage-services.yaml: -------------------------------------------------------------------------------- 1 | Version: "2012-10-17" 2 | Statement: 3 | - Sid: AllowGlobalUnrestrictedResourceActionsWhichIncurNoFees 4 | Effect: Allow 5 | Action: 6 | - s3:CreateAccessPoint* 7 | - s3:CreateBucket 8 | - s3:DeleteAccessPoint* 9 | - s3:DeleteBucket 10 | - s3:DeleteBucketOwnershipControls 11 | - s3:DeleteObject 12 | - s3:DeleteObjects 13 | - s3:DeleteObjectTagging 14 | - s3:DeleteObjectVersion 15 | - s3:DeleteObjectVersionTagging 16 | - s3:Get* 17 | - s3:HeadBucket 18 | - s3:HeadObject 19 | - s3:List* 20 | - s3:PutAccelerateConfiguration 21 | - s3:PutBucketAcl 22 | - s3:PutBucketLogging 23 | - s3:PutBucketNotification 24 | - s3:PutBucketObjectLockConfiguration 25 | - s3:PutBucketOwnershipControls 26 | - s3:PutBucketPolicy 27 | - s3:PutBucketPublicAccessBlock 28 | - s3:PutBucketRequestPayment 29 | - s3:PutBucketTagging 30 | - s3:PutBucketVersioning 31 | - s3:PutEncryptionConfiguration 32 | - s3:PutInventoryConfiguration 33 | - s3:PutLifecycleConfiguration 34 | - s3:PutObject 35 | - s3:PutObjectAcl 36 | - s3:PutObjectTagging 37 | - s3:PutObjectVersionTagging 38 | - s3:PutReplicationConfiguration 39 | - elasticfilesystem:CreateFileSystem 40 | - elasticfilesystem:CreateMountTarget 41 | - elasticfilesystem:CreateTags 42 | - elasticfilesystem:DeleteFileSystem 43 | - elasticfilesystem:DeleteMountTarget 44 | - elasticfilesystem:Describe* 45 | - elasticfilesystem:List* 46 | - elasticfilesystem:PutLifecycleConfiguration 47 | - elasticfilesystem:TagResource 48 | - elasticfilesystem:UntagResource 49 | - elasticfilesystem:UpdateFileSystem 50 | - backup:CreateBackupPlan 51 | - backup:CreateBackupSelection 52 | - backup:CreateBackupVault 53 | - backup:DeleteBackupPlan 54 | - backup:DeleteBackupSelection 55 | - backup:DeleteBackupVault 56 | - backup:Describe* 57 | - backup:GetBackup* 58 | - backup:List* 59 | - backup:TagResource 60 | - backup:UntagResource 61 | - backup:UpdateBackupPlan 62 | - backup-storage:MountCapsule 63 | - memorydb:Describe* 64 | - memorydb:List* 65 | Resource: "*" 66 | 67 | - Sid: AllowGlobalUnrestrictedResourceActionsWhichIncurFees 68 | Effect: Allow 69 | Action: 70 | - s3:PutMetricsConfiguration 71 | Resource: "*" 72 | 73 | - Sid: AllowRegionalRestrictedResourceActionsWhichIncurFees 74 | Effect: Allow 75 | Action: 76 | - memorydb:CreateCluster 77 | - memorydb:CreateSnapshot 78 | Resource: "*" 79 | Condition: 80 | StringEquals: 81 | aws:RequestedRegion: 82 | - '{{ aws_region }}' 83 | 84 | - Sid: AllowRegionalRestrictedResourceActionsWhichIncurNoFees 85 | Effect: Allow 86 | Action: 87 | - memorydb:CreateParameterGroup 88 | - memorydb:CreateSubnetGroup 89 | - memorydb:CreateUser 90 | - memorydb:CreateACL 91 | - memorydb:DeleteACL 92 | - memorydb:DeleteCluster 93 | - memorydb:DeleteParameterGroup 94 | - memorydb:DeleleSnapshot 95 | - memorydb:DeleteSubnetGroup 96 | - memorydb:DeleteUser 97 | - memorydb:TagResource 98 | Resource: 99 | - 'arn:aws:memorydb:{{ aws_region }}:{{ aws_account_id }}:*' 100 | -------------------------------------------------------------------------------- /aws/requirements.txt: -------------------------------------------------------------------------------- 1 | # note: The dependencies will be included in the Lambda package. 2 | boto3==1.25.5 3 | botocore==1.28.5 4 | certifi==2022.9.24 5 | charset-normalizer==2.1.1 6 | idna==3.4 7 | jmespath==1.0.1 8 | python-dateutil==2.8.2 9 | requests==2.28.1 10 | s3transfer==0.6.0 11 | six==1.16.0 12 | urllib3==1.26.12 13 | # The PyYAML requirement is only used for the cleanup.py script, 14 | # so shouldn't be included here. Including it here will put it 15 | # into the venv which gets packaged up for the Lambda function, 16 | # which isn't needed. 17 | # PyYAML==6.0 18 | -------------------------------------------------------------------------------- /aws/terminator-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "dynamodb:CreateTable", 8 | "dynamodb:PutItem", 9 | "dynamodb:DescribeTable", 10 | "dynamodb:DeleteItem", 11 | "dynamodb:GetItem", 12 | "dynamodb:Scan", 13 | "dynamodb:BatchWriteItem" 14 | ], 15 | "Resource": "arn:aws:dynamodb:*:{{ aws_account_id }}:table/{{ api_name.replace('-', '_') }}_resources_{{ stage }}" 16 | }, 17 | { 18 | "Effect": "Allow", 19 | "Action": "sts:AssumeRole", 20 | "Resource": "arn:aws:iam::{{ test_account_id }}:role/{{ api_name }}-test-{{ stage }}" 21 | }, 22 | { 23 | "Effect": "Allow", 24 | "Action": [ 25 | "logs:CreateLogGroup", 26 | "logs:CreateLogStream", 27 | "logs:PutLogEvents" 28 | ], 29 | "Resource": "arn:aws:logs:*:*:*" 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /aws/terminator.yml: -------------------------------------------------------------------------------- 1 | - hosts: localhost 2 | collections: 3 | - mattclay.aws 4 | gather_facts: False 5 | vars: 6 | aws_region: "us-east-1" 7 | stage: "prod" 8 | terminator_policy: "{{ lookup('template', 'terminator-policy.json') }}" 9 | packaging_dir: "{{ playbook_dir }}/../.cache/packaging" 10 | tasks: 11 | - name: load config 12 | tags: always 13 | include_vars: 14 | file: config.yml 15 | - name: check config 16 | tags: always 17 | assert: 18 | that: 'test_account_id != lambda_account_id' 19 | - name: get aws account facts 20 | tags: always 21 | aws_account_facts: 22 | - name: show configuration 23 | tags: always 24 | debug: msg="aws_region={{ aws_region }}, stage={{ stage }}, aws_account_id={{ aws_account_id }}" 25 | - name: verify aws_account_id matches lambda_account_id 26 | tags: always 27 | assert: 28 | that: "aws_account_id == lambda_account_id" 29 | - name: create iam role for terminator functions 30 | tags: iam 31 | community.aws.iam_role: 32 | name: "{{ api_name }}-terminator-{{ stage }}" 33 | description: "iam role for terminator functions" 34 | state: present 35 | assume_role_policy_document: 36 | Version: "2012-10-17" 37 | Statement: 38 | Action: "sts:AssumeRole" 39 | Principal: 40 | Service: "lambda.amazonaws.com" 41 | Effect: "Allow" 42 | - name: create iam policy for terminator functions 43 | tags: iam 44 | iam_policy: 45 | iam_type: role 46 | iam_name: "{{ api_name }}-terminator-{{ stage }}" 47 | policy_name: "{{ api_name }}-terminator-{{ stage }}" 48 | state: present 49 | policy_json: "{{ terminator_policy | to_nice_json }}" 50 | - name: create virtualenv with terminator requirements 51 | tags: lambda 52 | pip: 53 | requirements: "{{ playbook_dir }}/requirements.txt" 54 | virtualenv: "{{ packaging_dir }}/terminator-requirements/python" 55 | virtualenv_python: python3.9 56 | - name: package terminator requirements 57 | tags: lambda 58 | lambda_package: 59 | src: "{{ packaging_dir }}/terminator-requirements" 60 | dest: "{{ packaging_dir }}/terminator-requirements.zip" 61 | include: 62 | - "{{ packaging_dir }}/terminator-requirements/python/lib/python3.9/site-packages/*" 63 | exclude: 64 | # pre-compiled bytecode 65 | - "*.pyc" 66 | # packaging information not needed at runtime 67 | - "*.dist-info/*" 68 | # only used for botocore documentation generation 69 | - "{{ packaging_dir }}/terminator-requirements/python/lib/python3.9/site-packages/docutils/*" 70 | # installed during creation of the virtualenv 71 | - "{{ packaging_dir }}/terminator-requirements/python/lib/python3.9/site-packages/pip/*" 72 | - "{{ packaging_dir }}/terminator-requirements/python/lib/python3.9/site-packages/wheel/*" 73 | - "{{ packaging_dir }}/terminator-requirements/python/lib/python3.9/site-packages/setuptools/*" 74 | - "{{ packaging_dir }}/terminator-requirements/python/lib/python3.9/site-packages/pkg_resources/*" 75 | - "{{ packaging_dir }}/terminator-requirements/python/lib/python3.9/site-packages/easy_install.py" 76 | - name: publish terminator requirements layer 77 | tags: lambda 78 | lambda_layer: 79 | name: "{{ api_name }}-terminator-requirements" 80 | description: "Python requirements for {{ api_name }}-terminator" 81 | compatible_runtimes: 82 | - python3.9 83 | path: "{{ packaging_dir }}/terminator-requirements.zip" 84 | license_info: GPL-3.0-only 85 | region: "{{ aws_region }}" 86 | state: present 87 | register: terminator_requirements_layer 88 | - name: create terminator package 89 | tags: lambda 90 | lambda_package: 91 | src: "{{ playbook_dir }}" 92 | dest: "{{ packaging_dir }}/terminator.zip" 93 | include: 94 | - "{{ playbook_dir }}/terminator_lambda.py" 95 | - "{{ playbook_dir }}/terminator/*.py" 96 | - name: deploy terminator package 97 | tags: lambda 98 | lambda: 99 | region: "{{ aws_region }}" 100 | name: "{{ api_name }}-terminator" 101 | local_path: "{{ packaging_dir }}/terminator.zip" 102 | runtime: python3.9 103 | timeout: 120 104 | handler: terminator_lambda.lambda_handler 105 | memory_size: 256 106 | role: "{{ api_name }}-terminator-{{ stage }}" 107 | publish: True 108 | qualifier: "{{ stage }}" 109 | environment: 110 | TEST_ACCOUNT_ID: "{{ test_account_id }}" 111 | API_NAME: "{{ api_name }}" 112 | layers: 113 | - "{{ terminator_requirements_layer.layer.layer_version_arn }}" 114 | register: terminator_function 115 | - name: alias terminator functions 116 | tags: lambda 117 | lambda_alias: 118 | region: "{{ aws_region }}" 119 | state: present 120 | name: "{{ stage }}" 121 | description: "{{ stage }} alias" 122 | function_name: "{{ terminator_function.meta.function_name }}" 123 | version: "{{ terminator_function.meta.version }}" 124 | - name: schedule terminator event 125 | tags: schedule 126 | cloudwatch_event: 127 | region: "{{ aws_region }}" 128 | rule_name: "{{ api_name }}-terminator-{{ stage }}" 129 | schedule_expression: rate(5 minutes) 130 | function_name: "{{ api_name }}-terminator:{{ stage }}" 131 | register: event 132 | - name: schedule terminator event permissions 133 | tags: schedule 134 | lambda_policy: 135 | region: "{{ aws_region }}" 136 | function_name: "{{ api_name }}-terminator:{{ stage }}" 137 | source_arn: "arn:aws:events:{{ aws_region }}:{{ aws_account_id }}:rule/{{ api_name }}-terminator-{{ stage }}" 138 | principal_service: events.amazonaws.com 139 | -------------------------------------------------------------------------------- /aws/terminator/__init__.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import datetime 3 | import inspect 4 | import json 5 | import logging 6 | import os 7 | import re 8 | import traceback 9 | import typing 10 | 11 | from boto3.dynamodb.conditions import Attr 12 | import boto3 13 | import botocore 14 | import botocore.client 15 | import botocore.exceptions 16 | import dateutil.tz 17 | 18 | logger = logging.getLogger('cleanup') 19 | 20 | AWS_REGION = 'us-east-1' 21 | 22 | T = typing.TypeVar('T') 23 | 24 | 25 | def log_exception(message: str, *args, level: int = logging.ERROR) -> None: 26 | payload = dict( 27 | message=(message % args).strip(), 28 | traceback=traceback.format_exc().strip(), 29 | ) 30 | 31 | logger.log(level, json.dumps(payload)) 32 | 33 | 34 | def import_plugins() -> None: 35 | skip_files = ('__init__.py',) 36 | import_names = [os.path.splitext(name)[0] for name in os.listdir(os.path.dirname(__file__)) if name.endswith('.py') and name not in skip_files] 37 | for import_name in import_names: 38 | __import__(f'terminator.{import_name}') 39 | 40 | 41 | def cleanup(stage: str, check: bool, force: bool, api_name: str, test_account_id: str, targets: typing.Optional[typing.List[str]] = None) -> None: 42 | kvs.domain_name = re.sub(r'[^a-zA-Z0-9]+', '_', f'{api_name}-resources-{stage}') 43 | kvs.initialize() 44 | 45 | cleanup_test_account(stage, check, force, api_name, test_account_id, targets) 46 | 47 | if not targets or 'Database' in targets: 48 | cleanup_database(check, force) 49 | 50 | 51 | def assume_session(role: str, session_name: str) -> boto3.Session: 52 | sts = boto3.client('sts') 53 | credentials = sts.assume_role( 54 | RoleArn=role, RoleSessionName=session_name).get('Credentials') 55 | return boto3.Session( 56 | aws_access_key_id=credentials['AccessKeyId'], 57 | aws_secret_access_key=credentials['SecretAccessKey'], 58 | aws_session_token=credentials['SessionToken']) 59 | 60 | 61 | def process_instance(instance: 'Terminator', check: bool, force: bool = False) -> str: 62 | if instance.ignore: 63 | status = 'ignored' 64 | elif force: 65 | status = terminate(instance, check) 66 | elif instance.age is None: 67 | status = 'unsupported' 68 | elif instance.stale: 69 | status = terminate(instance, check) 70 | else: 71 | status = 'skipped' 72 | return status 73 | 74 | 75 | def cleanup_test_account(stage: str, check: bool, force: bool, api_name: str, test_account_id: str, targets: typing.Optional[typing.List[str]] = None) -> None: 76 | role = f'arn:aws:iam::{test_account_id}:role/{api_name}-test-{stage}' 77 | credentials = assume_session(role, 'cleanup') 78 | 79 | for terminator_type in sorted(get_concrete_subclasses(Terminator), key=lambda value: value.__name__): 80 | if targets and terminator_type.__name__ not in targets: 81 | continue 82 | 83 | # noinspection PyBroadException 84 | try: 85 | # noinspection PyUnresolvedReferences 86 | instances = terminator_type.create(credentials) 87 | 88 | for instance in instances: 89 | status = process_instance(instance, check, force) 90 | if instance.ignore: 91 | logger.debug('%s %s', status, instance) 92 | else: 93 | logger.info('%s %s', status, instance) 94 | except Exception: # pylint: disable=broad-except 95 | log_exception('exception processing resource type: %s', terminator_type) 96 | 97 | 98 | def cleanup_database(check: bool, force: bool) -> None: 99 | scan_options = {} 100 | 101 | if not force: 102 | now = datetime.datetime.utcnow().replace(tzinfo=dateutil.tz.tzutc(), microsecond=0) - datetime.timedelta(minutes=60) 103 | scan_options['FilterExpression'] = Attr('created_time').lt(now.isoformat()) 104 | 105 | scan_options['ProjectionExpression'] = kvs.primary_key 106 | scan_options['Limit'] = 25 107 | scan_options['ConsistentRead'] = False 108 | 109 | result = kvs.table.scan( 110 | **scan_options 111 | ) 112 | 113 | if 'Items' not in result: 114 | return 115 | 116 | items = result['Items'] 117 | 118 | if items and not check: 119 | with kvs.table.batch_writer(): 120 | for item in items: 121 | kvs.table.delete_item(Key=item) 122 | 123 | if check: 124 | status = 'checked' 125 | else: 126 | status = 'purged' 127 | 128 | for item in items: 129 | logger.info('%s database item: %s', status, item['id']) 130 | 131 | 132 | def terminate(instance: 'Terminator', check: bool) -> str: 133 | if check: 134 | return 'checked' 135 | 136 | # noinspection PyBroadException 137 | try: 138 | instance.terminate() 139 | instance.cleanup() 140 | except botocore.exceptions.ClientError as ex: 141 | error_code = ex.response['Error']['Code'] 142 | 143 | if error_code == 'TooManyRequestsException': 144 | log_exception('error "%s" terminating %s', error_code, instance, level=logging.WARNING) 145 | else: 146 | log_exception('error "%s" terminating %s', error_code, instance) 147 | except Exception: # pylint: disable=broad-except 148 | log_exception('exception terminating %s', instance) 149 | 150 | return 'terminated' 151 | 152 | 153 | def get_concrete_subclasses(class_type: typing.Type[T]) -> typing.Set[typing.Type[T]]: 154 | subclasses: typing.Set[typing.Type[T]] = set() 155 | queue: typing.List[typing.Type[T]] = [class_type] 156 | 157 | while queue: 158 | parent = queue.pop() 159 | 160 | for child in parent.__subclasses__(): 161 | if child not in subclasses: 162 | queue.append(child) 163 | if not inspect.isabstract(child): 164 | subclasses.add(child) 165 | 166 | return subclasses 167 | 168 | 169 | def get_account_id(session: boto3.Session) -> str: 170 | return session.client('sts').get_caller_identity().get('Account') 171 | 172 | 173 | def get_tag_dict_from_tag_list(tag_list: typing.Optional[typing.List[typing.Dict[str, str]]]) -> typing.Dict[str, str]: 174 | if tag_list is None: 175 | return {} 176 | 177 | return dict((tag['Key'], tag['Value']) for tag in tag_list) 178 | 179 | 180 | class Terminator(abc.ABC): 181 | """Base class for classes which find and terminate AWS resources.""" 182 | _default_vpc = None # safe as long as executing only within a single region 183 | 184 | def __init__(self, client: botocore.client.BaseClient, instance: typing.Dict[str, typing.Any]): 185 | self.client = client 186 | self.instance = instance 187 | self.now = datetime.datetime.utcnow().replace(tzinfo=dateutil.tz.tzutc(), microsecond=0) 188 | 189 | @staticmethod 190 | @abc.abstractmethod 191 | def create(credentials: boto3.Session) -> typing.List['Terminator']: 192 | pass 193 | 194 | @property 195 | def age_limit(self) -> datetime.timedelta: 196 | return datetime.timedelta(minutes=20) 197 | 198 | @property 199 | def id(self) -> typing.Optional[str]: 200 | return None 201 | 202 | @property 203 | @abc.abstractmethod 204 | def name(self) -> str: 205 | pass 206 | 207 | @property 208 | @abc.abstractmethod 209 | def created_time(self) -> typing.Optional[datetime.datetime]: 210 | pass 211 | 212 | @property 213 | def ignore(self) -> bool: 214 | return False 215 | 216 | @abc.abstractmethod 217 | def terminate(self) -> None: 218 | """Terminate or delete the AWS resource.""" 219 | 220 | def cleanup(self) -> None: 221 | """Cleanup to perform after termination.""" 222 | 223 | @property 224 | def age(self) -> typing.Optional[datetime.timedelta]: 225 | return self.now - self.created_time if self.created_time else None 226 | 227 | @property 228 | def stale(self) -> bool: 229 | return self.age > self.age_limit if self.age else False 230 | 231 | def __str__(self) -> str: 232 | # noinspection PyBroadException 233 | try: 234 | if self.id: 235 | extra = f'id={self.id} ' 236 | else: 237 | extra = '' 238 | 239 | return f'{type(self).__name__}: name={self.name}, {extra}age={self.age}, stale={self.stale}' 240 | except Exception: # pylint: disable=broad-except 241 | log_exception('exception converting %s to string', type(self).__name__) 242 | return type(self).__name__ 243 | 244 | @staticmethod 245 | def _create(session: boto3.Session, instance_type: typing.Type['Terminator'], client_name: str, 246 | describe_lambda: typing.Callable[[botocore.client.BaseClient], typing.List[typing.Dict[str, typing.Any]]]) -> typing.List['Terminator']: 247 | client = session.client(client_name, region_name=AWS_REGION) 248 | instances = describe_lambda(client) 249 | terminators = [instance_type(client, instance) for instance in instances] 250 | logger.debug('located %s: count=%d', instance_type.__name__, len(terminators)) 251 | 252 | return terminators 253 | 254 | @property 255 | def default_vpc(self) -> typing.Dict[str, str]: 256 | if self._default_vpc is None: 257 | vpcs = self.client.describe_vpcs(Filters=[{'Name': 'isDefault', 'Values': ['true']}])['Vpcs'] 258 | 259 | if vpcs: 260 | self._default_vpc = vpcs[0] # found default VPC 261 | else: 262 | self._default_vpc = {} # no default VPC 263 | 264 | return self._default_vpc 265 | 266 | def is_vpc_default(self, vpc_id: str) -> bool: 267 | return self.default_vpc.get('VpcId') == vpc_id 268 | 269 | 270 | class DbTerminator(Terminator): 271 | """Base class for classes which find and terminate AWS resources with age tracked via DynamoDB.""" 272 | def __init__(self, client: botocore.client.BaseClient, instance: typing.Dict[str, typing.Any]): 273 | super().__init__(client, instance) 274 | 275 | self._kvs_key = None 276 | self._kvs_value = None 277 | self._created_time = None 278 | 279 | if self.ignore: 280 | return 281 | 282 | # noinspection PyBroadException 283 | try: 284 | self._kvs_key = f'{type(self).__name__}:{self.id or self.name}' 285 | self._kvs_value = kvs.get(self._kvs_key) 286 | 287 | if not self._kvs_value: 288 | self._kvs_value = self.now.isoformat() 289 | kvs.set(self._kvs_key, self._kvs_value) 290 | 291 | self._created_time = datetime.datetime.strptime(self._kvs_value.replace('+00:00', ''), '%Y-%m-%dT%H:%M:%S').replace(tzinfo=dateutil.tz.tzutc()) 292 | except Exception: # pylint: disable=broad-except 293 | log_exception('exception accessing key/value store: %s', self) 294 | 295 | @property 296 | @abc.abstractmethod 297 | def name(self) -> str: 298 | pass 299 | 300 | @property 301 | def created_time(self) -> typing.Optional[datetime.datetime]: 302 | return self._created_time 303 | 304 | @abc.abstractmethod 305 | def terminate(self) -> None: 306 | """Terminate or delete the AWS resource.""" 307 | 308 | def cleanup(self) -> None: 309 | """Cleanup to perform after termination.""" 310 | if not self._kvs_key or not self._kvs_value: 311 | logger.warning('skipping cleanup due to missing key/value data: %s', self) 312 | return 313 | 314 | kvs.delete(self._kvs_key) 315 | 316 | 317 | class KeyValueStore: 318 | """ DynamoDB data store for the AWS terminator """ 319 | def __init__(self, domain_name: typing.Optional[str] = None): 320 | self.ddb = None 321 | self.domain_name = domain_name 322 | self.table = None 323 | self.primary_key = 'id' 324 | self.initialized = False 325 | 326 | def initialize(self) -> None: 327 | """Deferred initialization of the DynamoDB database.""" 328 | if self.initialized: 329 | return 330 | 331 | self.ddb = boto3.resource('dynamodb', region_name=AWS_REGION) 332 | 333 | try: 334 | self.table = self.ddb.Table(self.domain_name) 335 | if self.table.table_status == 'DELETING': 336 | self.table.wait_until_not_exists() 337 | self.create_table() 338 | except botocore.exceptions.ClientError as ex: 339 | if ex.response['Error']['Code'] == 'ResourceNotFoundException': 340 | self.create_table() 341 | else: 342 | raise ex 343 | 344 | self.initialized = True 345 | 346 | def get(self, key: str) -> str: 347 | self.initialize() 348 | 349 | item = self.table.get_item( 350 | Key={self.primary_key: key}, 351 | ProjectionExpression='created_time', 352 | ).get('Item', {}) 353 | 354 | return item.get('created_time') 355 | 356 | def set(self, key: str, value: str) -> None: 357 | self.initialize() 358 | 359 | # Don't replace an existing entry 360 | expression = Attr(self.primary_key).ne(key) 361 | 362 | attributes = { 363 | self.primary_key: key, 364 | 'created_time': value, 365 | } 366 | 367 | self.table.put_item( 368 | Item=attributes, 369 | ConditionExpression=expression, 370 | ) 371 | 372 | def create_table(self) -> None: 373 | """Creates a new DynamoDB database.""" 374 | self.table = self.ddb.create_table( 375 | TableName=self.domain_name, 376 | AttributeDefinitions=[{ 377 | 'AttributeName': self.primary_key, 378 | 'AttributeType': 'S' 379 | }], 380 | KeySchema=[{ 381 | 'AttributeName': self.primary_key, 382 | 'KeyType': 'HASH' 383 | }], 384 | BillingMode='PAY_PER_REQUEST', 385 | ) 386 | self.table.wait_until_exists() 387 | 388 | def delete(self, key: str) -> None: 389 | self.initialize() 390 | 391 | self.table.delete_item( 392 | Key={ 393 | self.primary_key: key 394 | }, 395 | ) 396 | 397 | 398 | import_plugins() 399 | 400 | kvs = KeyValueStore() 401 | -------------------------------------------------------------------------------- /aws/terminator/application_security.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import datetime 3 | 4 | from . import DbTerminator, Terminator 5 | 6 | 7 | class Waf(DbTerminator): 8 | @property 9 | def age_limit(self): 10 | return datetime.timedelta(minutes=30) 11 | 12 | @property 13 | def change_token(self): 14 | return self.client.get_change_token()['ChangeToken'] 15 | 16 | @property 17 | def name(self): 18 | return self.instance['Name'] 19 | 20 | @abc.abstractmethod 21 | def terminate(self): 22 | """Terminate or delete the AWS resource.""" 23 | 24 | 25 | class WafWebAcl(Waf): 26 | @staticmethod 27 | def create(credentials): 28 | return Terminator._create(credentials, WafWebAcl, 'waf', lambda client: client.list_web_acls()['WebACLs']) 29 | 30 | @property 31 | def age_limit(self): 32 | # Try to delete WafWebAcl first, because WafRule objects cannot be deleted if used in any WebACL 33 | return datetime.timedelta(minutes=20) 34 | 35 | @property 36 | def id(self): 37 | return self.instance['WebACLId'] 38 | 39 | def terminate(self): 40 | updates = [{'Action': 'DELETE', 'ActivatedRule': i} for i in 41 | self.client.get_web_acl(WebACLId=self.id)['WebACL']['Rules']] 42 | if updates: 43 | self.client.update_web_acl(WebACLId=self.id, ChangeToken=self.change_token, Updates=updates) 44 | self.client.delete_web_acl(WebACLId=self.id, ChangeToken=self.change_token) 45 | 46 | 47 | class WafRule(Waf): 48 | @staticmethod 49 | def create(credentials): 50 | return Terminator._create(credentials, WafRule, 'waf', lambda client: client.list_rules()['Rules']) 51 | 52 | @property 53 | def id(self): 54 | return self.instance['RuleId'] 55 | 56 | def terminate(self): 57 | updates = [{'Action': 'DELETE', 'Predicates': i} for i in 58 | self.client.get_rule(RuleId=self.id)['Rule']['Predicates']] 59 | if updates: 60 | self.client.update_rule(WebACLId=self.id, ChangeToken=self.change_token, Updates=updates) 61 | self.client.delete_rule(RuleId=self.id, ChangeToken=self.change_token) 62 | 63 | 64 | class WafXssMatchSet(Waf): 65 | @staticmethod 66 | def create(credentials): 67 | return Terminator._create(credentials, WafXssMatchSet, 'waf', lambda client: client.list_xss_match_sets()['XssMatchSets']) 68 | 69 | @property 70 | def id(self): 71 | return self.instance['XssMatchSetId'] 72 | 73 | def terminate(self): 74 | updates = [{'Action': 'DELETE', 'XssMatchTuple': i} for i in 75 | self.client.get_xss_match_set(XssMatchSetId=self.id)['XssMatchSet']['XssMatchTuples']] 76 | if updates: 77 | self.client.update_xss_match_set(XssMatchSetId=self.id, ChangeToken=self.change_token, Updates=updates) 78 | self.client.delete_xss_match_set(XssMatchSetId=self.id, ChangeToken=self.change_token) 79 | 80 | 81 | class WafGeoMatchSet(Waf): 82 | @staticmethod 83 | def create(credentials): 84 | return Terminator._create(credentials, WafGeoMatchSet, 'waf', lambda client: client.list_geo_match_sets()['GeoMatchSets']) 85 | 86 | @property 87 | def id(self): 88 | return self.instance['GeoMatchSetId'] 89 | 90 | def terminate(self): 91 | updates = [{'Action': 'DELETE', 'GeoMatchConstraint': i} for i in 92 | self.client.get_geo_match_set(GeoMatchSetId=self.id)['GeoMatchSet']['GeoMatchConstraints']] 93 | if updates: 94 | self.client.update_geo_match_set(GeoMatchSetId=self.id, ChangeToken=self.change_token, Updates=updates) 95 | self.client.delete_geo_match_set(GeoMatchSetId=self.id, ChangeToken=self.change_token) 96 | 97 | 98 | class WafSqlInjectionMatchSet(Waf): 99 | @staticmethod 100 | def create(credentials): 101 | return Terminator._create(credentials, WafSqlInjectionMatchSet, 'waf', lambda client: client.list_sql_injection_match_sets()['SqlInjectionMatchSets']) 102 | 103 | @property 104 | def id(self): 105 | return self.instance['SqlInjectionMatchSetId'] 106 | 107 | def terminate(self): 108 | updates = [{'Action': 'DELETE', 'SqlInjectionMatchTuple': i} for i in 109 | self.client.get_sql_injection_match_set(SqlInjectionMatchSetId=self.id)['SqlInjectionMatchSet']['SqlInjectionMatchTuples']] 110 | if updates: 111 | self.client.update_sql_injection_match_set(SqlInjectionMatchSetId=self.id, ChangeToken=self.change_token, Updates=updates) 112 | self.client.delete_sql_injection_match_set(SqlInjectionMatchSetId=self.id, ChangeToken=self.change_token) 113 | 114 | 115 | class WafIpSet(Waf): 116 | @staticmethod 117 | def create(credentials): 118 | return Terminator._create(credentials, WafIpSet, 'waf', lambda client: client.list_ip_sets()['IPSets']) 119 | 120 | @property 121 | def id(self): 122 | return self.instance['IPSetId'] 123 | 124 | def terminate(self): 125 | updates = [{'Action': 'DELETE', 'IPSetDescriptor': i} for i in 126 | self.client.get_ip_set(IPSetId=self.id)['IPSet']['IPSetDescriptors']] 127 | if updates: 128 | self.client.update_ip_set(IPSetId=self.id, ChangeToken=self.change_token, Updates=updates) 129 | self.client.delete_ip_set(IPSetId=self.id, ChangeToken=self.change_token) 130 | 131 | 132 | class WafSizeConstraintSet(Waf): 133 | @staticmethod 134 | def create(credentials): 135 | return Terminator._create(credentials, WafSizeConstraintSet, 'waf', lambda client: client.list_size_constraint_sets()['SizeConstraintSets']) 136 | 137 | @property 138 | def id(self): 139 | return self.instance['SizeConstraintSetId'] 140 | 141 | def terminate(self): 142 | updates = [{'Action': 'DELETE', 'SizeConstraint': i} for i in 143 | self.client.get_size_constraint_set(SizeConstraintSetId=self.id)['SizeConstraintSet']['SizeConstraints']] 144 | if updates: 145 | self.client.update_size_constraint_set(SizeConstraintSetId=self.id, ChangeToken=self.change_token, Updates=updates) 146 | self.client.delete_size_constraint_set(SizeConstraintSetId=self.id, ChangeToken=self.change_token) 147 | 148 | 149 | class WafByteMatchSet(Waf): 150 | @staticmethod 151 | def create(credentials): 152 | return Terminator._create(credentials, WafByteMatchSet, 'waf', lambda client: client.list_byte_match_sets()['ByteMatchSets']) 153 | 154 | @property 155 | def id(self): 156 | return self.instance['ByteMatchSetId'] 157 | 158 | def terminate(self): 159 | updates = [{'Action': 'DELETE', 'ByteMatchTuple': i} for i in 160 | self.client.get_byte_match_set(ByteMatchSetId=self.id)['ByteMatchSet']['ByteMatchTuples']] 161 | if updates: 162 | self.client.update_byte_match_set(ByteMatchSetId=self.id, ChangeToken=self.change_token, Updates=updates) 163 | self.client.delete_byte_match_set(ByteMatchSetId=self.id, ChangeToken=self.change_token) 164 | 165 | 166 | class WafRegexMatchSet(Waf): 167 | @staticmethod 168 | def create(credentials): 169 | return Terminator._create(credentials, WafRegexMatchSet, 'waf', lambda client: client.list_regex_match_sets()['RegexMatchSets']) 170 | 171 | @property 172 | def id(self): 173 | return self.instance['RegexMatchSetId'] 174 | 175 | def terminate(self): 176 | updates = [{'Action': 'DELETE', 'RegexMatchTuple': i} for i in 177 | self.client.get_regex_match_set(RegexMatchSetId=self.id)['RegexMatchSet']['RegexMatchTuples']] 178 | if updates: 179 | self.client.update_regex_match_set(RegexMatchSetId=self.id, ChangeToken=self.change_token, Updates=updates) 180 | self.client.delete_regex_match_set(RegexMatchSetId=self.id, ChangeToken=self.change_token) 181 | 182 | 183 | class WafRegexPatternSet(Waf): 184 | @staticmethod 185 | def create(credentials): 186 | return Terminator._create(credentials, WafRegexPatternSet, 'waf', lambda client: client.list_regex_pattern_sets()['RegexPatternSets']) 187 | 188 | @property 189 | def id(self): 190 | return self.instance['RegexPatternSetId'] 191 | 192 | def terminate(self): 193 | updates = [{'Action': 'DELETE', 'RegexPatternString': i} for i in 194 | self.client.get_regex_pattern_set(RegexPatternSetId=self.id)['RegexPatternSet']['RegexPatternStrings']] 195 | if updates: 196 | self.client.update_regex_pattern_set(RegexPatternSetId=self.id, ChangeToken=self.change_token, Updates=updates) 197 | self.client.delete_regex_pattern_set(RegexPatternSetId=self.id, ChangeToken=self.change_token) 198 | 199 | 200 | class WafV2(DbTerminator): 201 | @property 202 | def id(self): 203 | return self.instance['Id'] 204 | 205 | @property 206 | def name(self): 207 | return self.instance['Name'] 208 | 209 | @property 210 | def lock_token(self): 211 | return self.instance['LockToken'] 212 | 213 | @property 214 | def scope(self): 215 | return self.instance['Scope'] 216 | 217 | @abc.abstractmethod 218 | def terminate(self): 219 | """Terminate or delete the AWS resource.""" 220 | 221 | 222 | class RegionalWafV2IpSet(WafV2): 223 | @staticmethod 224 | def create(credentials): 225 | return DbTerminator._create(credentials, RegionalWafV2IpSet, 'wafv2', lambda client: client.list_ip_sets(Scope='REGIONAL')['IPSets']) 226 | 227 | def terminate(self): 228 | self.client.delete_ip_set(Id=self.id, Name=self.name, LockToken=self.lock_token, Scope='REGIONAL') 229 | 230 | 231 | class CloudfrontWafV2IpSet(WafV2): 232 | @staticmethod 233 | def create(credentials): 234 | return DbTerminator._create(credentials, CloudfrontWafV2IpSet, 'wafv2', lambda client: client.list_ip_sets(Scope='CLOUDFRONT')['IPSets']) 235 | 236 | def terminate(self): 237 | self.client.delete_ip_set(Id=self.id, Name=self.name, LockToken=self.lock_token, Scope='CLOUDFRONT') 238 | 239 | 240 | class RegionalWafV2RuleGroup(WafV2): 241 | @staticmethod 242 | def create(credentials): 243 | return DbTerminator._create(credentials, RegionalWafV2RuleGroup, 'wafv2', lambda client: client.list_rule_groups(Scope='REGIONAL')['RuleGroups']) 244 | 245 | def terminate(self): 246 | self.client.delete_rule_group(Id=self.id, Name=self.name, LockToken=self.lock_token, Scope='REGIONAL') 247 | 248 | 249 | class CloudfrontWafV2RuleGroup(WafV2): 250 | @staticmethod 251 | def create(credentials): 252 | return DbTerminator._create(credentials, CloudfrontWafV2RuleGroup, 'wafv2', lambda client: client.list_rule_groups(Scope='CLOUDFRONT')['RuleGroups']) 253 | 254 | def terminate(self): 255 | self.client.delete_rule_group(Id=self.id, Name=self.name, LockToken=self.lock_token, Scope='CLOUDFRONT') 256 | 257 | 258 | class RegionalWafV2WebAcl(WafV2): 259 | @staticmethod 260 | def create(credentials): 261 | return DbTerminator._create(credentials, RegionalWafV2WebAcl, 'wafv2', lambda client: client.list_web_acls(Scope='REGIONAL')['WebACLs']) 262 | 263 | def terminate(self): 264 | self.client.delete_web_acl(Id=self.id, Name=self.name, LockToken=self.lock_token, Scope='REGIONAL') 265 | 266 | 267 | class CloudfrontWafV2WebAcl(WafV2): 268 | @staticmethod 269 | def create(credentials): 270 | return DbTerminator._create(credentials, CloudfrontWafV2WebAcl, 'wafv2', lambda client: client.list_web_acls(Scope='CLOUDFRONT')['WebACLs']) 271 | 272 | def terminate(self): 273 | self.client.delete_web_acl(Id=self.id, Name=self.name, LockToken=self.lock_token, Scope='CLOUDFRONT') 274 | 275 | 276 | class InspectorAssessmentTemplate(DbTerminator): 277 | @staticmethod 278 | def create(credentials): 279 | return Terminator._create( 280 | credentials, InspectorAssessmentTemplate, 'inspector', 281 | lambda client: client.get_paginator('list_assessment_templates').paginate().build_full_result()['assessmentTemplateArns'] 282 | ) 283 | 284 | @property 285 | def id(self): 286 | return self.instance 287 | 288 | @property 289 | def name(self): 290 | return self.instance 291 | 292 | def terminate(self): 293 | self.client.delete_assessment_template(assessmentTemplateArn=self.id) 294 | 295 | 296 | class InspectorAssessmentTarget(DbTerminator): 297 | @staticmethod 298 | def create(credentials): 299 | return Terminator._create( 300 | credentials, InspectorAssessmentTarget, 'inspector', 301 | lambda client: client.get_paginator('list_assessment_targets').paginate().build_full_result()['assessmentTargetArns'] 302 | ) 303 | 304 | @property 305 | def id(self): 306 | return self.instance 307 | 308 | @property 309 | def name(self): 310 | return self.instance 311 | 312 | def terminate(self): 313 | self.client.delete_assessment_target(assessmentTargetArn=self.id) 314 | -------------------------------------------------------------------------------- /aws/terminator/application_services.py: -------------------------------------------------------------------------------- 1 | from datetime import timezone, datetime 2 | import time 3 | 4 | from . import DbTerminator, Terminator 5 | 6 | 7 | class Cloudformation(Terminator): 8 | @staticmethod 9 | def create(credentials): 10 | def paginate_stacks(client): 11 | return client.get_paginator('describe_stacks').paginate().build_full_result()['Stacks'] 12 | 13 | return Terminator._create(credentials, Cloudformation, 'cloudformation', paginate_stacks) 14 | 15 | @property 16 | def created_time(self): 17 | return self.instance['CreationTime'] 18 | 19 | @property 20 | def name(self): 21 | return self.instance['StackName'] 22 | 23 | def terminate(self): 24 | self.client.update_termination_protection(StackName=self.name, EnableTerminationProtection=False) 25 | self.client.delete_stack(StackName=self.name) 26 | 27 | 28 | class CloudWatchLogGroup(Terminator): 29 | @staticmethod 30 | def create(credentials): 31 | return Terminator._create(credentials, CloudWatchLogGroup, 'logs', lambda client: client.describe_log_groups()['logGroups']) 32 | 33 | @property 34 | def name(self): 35 | return self.instance['logGroupName'] 36 | 37 | @property 38 | def created_time(self): 39 | # self.instance['creationTime'] is the number of milliseconds after Jan 1, 1970 00:00:00 UTC 40 | milliseconds = self.instance['creationTime'] 41 | return datetime.fromtimestamp(milliseconds/1000.0, tz=timezone.utc) 42 | 43 | def terminate(self): 44 | self.client.delete_log_group(logGroupName=self.name) 45 | 46 | 47 | class CodeBuild(Terminator): 48 | 49 | @staticmethod 50 | def create(credentials): 51 | def paginate_projects(client): 52 | project_names = client.get_paginator( 53 | 'list_projects').paginate().build_full_result()['projects'] 54 | 55 | if not project_names: 56 | return [] 57 | 58 | projects = client.batch_get_projects( 59 | names=project_names).get('projects', ()) 60 | return [ 61 | {'name': p['name'], 'created': p['created']} for p in projects] 62 | 63 | return Terminator._create( 64 | credentials, CodeBuild, 'codebuild', 65 | paginate_projects) 66 | 67 | @property 68 | def created_time(self): 69 | return self.instance['created'] 70 | 71 | @property 72 | def id(self): 73 | return self.instance['name'] 74 | 75 | @property 76 | def name(self): 77 | return self.instance['name'] 78 | 79 | def terminate(self): 80 | self.client.delete_project(name=self.instance['name']) 81 | 82 | 83 | class CodeCommitRepository(DbTerminator): 84 | @staticmethod 85 | def create(credentials): 86 | def paginate_repositories(client): 87 | return client.get_paginator('list_repositories').paginate().build_full_result()['repositories'] 88 | 89 | return Terminator._create(credentials, CodeCommitRepository, 'codecommit', paginate_repositories) 90 | 91 | @property 92 | def id(self): 93 | return self.instance['repositoryId'] 94 | 95 | @property 96 | def name(self): 97 | return self.instance['repositoryName'] 98 | 99 | def terminate(self): 100 | self.client.delete_repository(repositoryName=self.name) 101 | 102 | 103 | class CodePipeline(Terminator): 104 | 105 | @staticmethod 106 | def create(credentials): 107 | return Terminator._create( 108 | credentials, CodePipeline, 'codepipeline', 109 | lambda client: client.list_pipelines().get('pipelines', ())) 110 | 111 | @property 112 | def created_time(self): 113 | return self.instance['created'] 114 | 115 | @property 116 | def id(self): 117 | return self.instance['name'] 118 | 119 | @property 120 | def name(self): 121 | return self.instance['name'] 122 | 123 | def terminate(self): 124 | self.client.delete_pipeline(name=self.name) 125 | 126 | 127 | class Efs(Terminator): 128 | @staticmethod 129 | def create(credentials): 130 | return Terminator._create(credentials, Efs, 'efs', lambda client: client.describe_file_systems()['FileSystems']) 131 | 132 | @property 133 | def id(self): 134 | return self.instance['FileSystemId'] 135 | 136 | @property 137 | def name(self): 138 | return self.instance['Name'] 139 | 140 | @property 141 | def created_time(self): 142 | return self.instance['CreationTime'] 143 | 144 | def terminate(self): 145 | # Cannot delete file system if in use: delete mounts targets first 146 | for mount_target in self.client.describe_mount_targets(FileSystemId=self.id)['MountTargets']: 147 | self.client.delete_mount_target(MountTargetId=mount_target['MountTargetId']) 148 | self.client.delete_file_system(FileSystemId=self.id) 149 | 150 | 151 | class KinesisStream(Terminator): 152 | @staticmethod 153 | def create(credentials): 154 | def paginate_streams(client): 155 | names = client.get_paginator('list_streams').paginate( 156 | PaginationConfig={ 157 | 'PageSize': 100, 158 | } 159 | ).build_full_result()['StreamNames'] 160 | 161 | if not names: 162 | return [] 163 | 164 | return [ 165 | client.describe_stream(StreamName=n)['StreamDescription'] for n in names 166 | ] 167 | 168 | return Terminator._create(credentials, KinesisStream, 'kinesis', paginate_streams) 169 | 170 | @property 171 | def created_time(self): 172 | return self.instance['StreamCreationTimestamp'] 173 | 174 | @property 175 | def id(self): 176 | return self.instance['StreamName'] 177 | 178 | @property 179 | def name(self): 180 | return self.instance['StreamName'] 181 | 182 | @property 183 | def ignore(self): 184 | return self.instance['StreamStatus'] == 'DELETING' 185 | 186 | def terminate(self): 187 | self.client.delete_stream( 188 | StreamName=self.instance['StreamName'], 189 | EnforceConsumerDeletion=True 190 | ) 191 | 192 | 193 | class SesIdentity(DbTerminator): 194 | @staticmethod 195 | def create(credentials): 196 | return Terminator._create(credentials, SesIdentity, 'ses', 197 | lambda client: client.list_identities()['Identities']) 198 | 199 | @property 200 | def id(self): 201 | return self.instance 202 | 203 | @property 204 | def name(self): 205 | return self.instance 206 | 207 | def terminate(self): 208 | self.client.delete_identity(Identity=self.id) 209 | 210 | 211 | class SesReceiptRuleSet(Terminator): 212 | @staticmethod 213 | def create(credentials): 214 | def _paginate_receipt_rule_sets(client): 215 | results = client.list_receipt_rule_sets() 216 | next_token = results.pop('NextToken', None) 217 | while next_token: 218 | # This operation can be made at most once/second, see 219 | # https://boto3.readthedocs.io/en/latest/reference/services/ses.html#SES.Client.list_receipt_rule_sets 220 | time.sleep(1) 221 | next_rule_sets = client.list_receipt_rule_sets(NextToken=next_token) 222 | results['RuleSets'].append(next_rule_sets['RuleSets']) 223 | next_token = next_rule_sets.pop('NextToken', None) 224 | return results['RuleSets'] 225 | return Terminator._create(credentials, SesReceiptRuleSet, 'ses', _paginate_receipt_rule_sets) 226 | 227 | @property 228 | def name(self): 229 | return self.instance['Name'] 230 | 231 | @property 232 | def created_time(self): 233 | return self.instance['CreatedTimestamp'] 234 | 235 | def terminate(self): 236 | self.client.delete_receipt_rule_set(RuleSetName=self.name) 237 | 238 | 239 | class Sns(DbTerminator): 240 | @staticmethod 241 | def create(credentials): 242 | return Terminator._create(credentials, Sns, 'sns', lambda client: client.list_topics()['Topics']) 243 | 244 | @property 245 | def id(self): 246 | return self.instance['TopicArn'] 247 | 248 | @property 249 | def name(self): 250 | return self.instance['TopicArn'] 251 | 252 | def terminate(self): 253 | self.client.delete_topic(TopicArn=self.id) 254 | 255 | 256 | class SqsQueue(DbTerminator): 257 | @staticmethod 258 | def create(credentials): 259 | return Terminator._create(credentials, SqsQueue, 'sqs', lambda client: client.list_queues().get('QueueUrls', [])) 260 | 261 | @property 262 | def id(self): 263 | return self.instance 264 | 265 | @property 266 | def name(self): 267 | return self.instance 268 | 269 | def terminate(self): 270 | self.client.delete_queue(QueueUrl=self.id) 271 | 272 | 273 | class SsmParameter(DbTerminator): 274 | @staticmethod 275 | def create(credentials): 276 | return Terminator._create(credentials, SsmParameter, 'ssm', lambda client: client.describe_parameters()['Parameters']) 277 | 278 | @property 279 | def id(self): 280 | return self.instance['Name'] 281 | 282 | @property 283 | def name(self): 284 | return self.instance['Name'] 285 | 286 | def terminate(self): 287 | self.client.delete_parameter(Name=self.id) 288 | 289 | 290 | class DynamoDb(DbTerminator): 291 | 292 | @staticmethod 293 | def create(credentials): 294 | 295 | def get_tables(client): 296 | table_names = client.get_paginator( 297 | 'list_tables').paginate().build_full_result().get('TableNames', ()) 298 | return table_names 299 | 300 | return Terminator._create(credentials, DynamoDb, 'dynamodb', get_tables) 301 | 302 | @property 303 | def id(self): 304 | return self.instance 305 | 306 | @property 307 | def name(self): 308 | return self.instance 309 | 310 | def terminate(self): 311 | return self.client.delete_table(TableName=self.instance) 312 | 313 | 314 | class StepFunctions(Terminator): 315 | @staticmethod 316 | def create(credentials): 317 | 318 | def get_state_machines(client): 319 | state_machines = client.get_paginator( 320 | 'list_state_machines').paginate().build_full_result().get('stateMachines', []) 321 | return state_machines 322 | 323 | return Terminator._create(credentials, StepFunctions, 'stepfunctions', get_state_machines) 324 | 325 | @property 326 | def created_time(self): 327 | return self.instance['creationDate'] 328 | 329 | @property 330 | def name(self): 331 | return self.instance['stateMachineArn'] 332 | 333 | def terminate(self): 334 | return self.client.delete_state_machine(stateMachineArn=self.name) 335 | 336 | 337 | class CloudWatchAlarm(DbTerminator): 338 | @staticmethod 339 | def create(credentials): 340 | return Terminator._create(credentials, CloudWatchAlarm, 'cloudwatch', lambda client: client.describe_alarms()['MetricAlarms']) 341 | 342 | @property 343 | def name(self): 344 | return self.instance['AlarmName'] 345 | 346 | def terminate(self): 347 | self.client.delete_alarms(AlarmNames=[self.name]) 348 | 349 | 350 | class SsmDocument(Terminator): 351 | @staticmethod 352 | def create(credentials): 353 | def get_ssm_documents(client): 354 | ssm_documents = client.get_paginator( 355 | 'list_documents').paginate(Filters=[{'Key': 'Owner', 'Values': ['self']}]).build_full_result().get('DocumentIdentifiers', []) 356 | return ssm_documents 357 | 358 | return Terminator._create(credentials, SsmDocument, 'ssm', get_ssm_documents) 359 | 360 | @property 361 | def created_time(self): 362 | return self.instance['CreatedDate'] 363 | 364 | @property 365 | def name(self): 366 | return self.instance['Name'] 367 | 368 | def terminate(self): 369 | self.client.delete_document(Name=self.name) 370 | 371 | 372 | class SsmSession(Terminator): 373 | @staticmethod 374 | def create(credentials): 375 | def get_ssm_sessions(client): 376 | ssm_sessions = client.get_paginator( 377 | 'describe_sessions').paginate(State='Active').build_full_result().get('Sessions', []) 378 | return ssm_sessions 379 | 380 | return Terminator._create(credentials, SsmSession, 'ssm', get_ssm_sessions) 381 | 382 | @property 383 | def created_time(self): 384 | return self.instance['StartDate'] 385 | 386 | @property 387 | def name(self): 388 | return self.instance['SessionId'] 389 | 390 | @property 391 | def id(self): 392 | return self.instance['SessionId'] 393 | 394 | def terminate(self): 395 | self.client.terminate_session(SessionId=self.name) 396 | 397 | 398 | class MqBroker(Terminator): 399 | @staticmethod 400 | def create(credentials): 401 | def get_mq_brokers(client): 402 | mq_brokers = client.get_paginator( 403 | 'list_brokers').paginate().build_full_result().get('BrokerSummaries', []) 404 | return mq_brokers 405 | 406 | return Terminator._create(credentials, MqBroker, 'mq', get_mq_brokers) 407 | 408 | @property 409 | def created_time(self): 410 | return self.instance['Created'] 411 | 412 | @property 413 | def name(self): 414 | return self.instance['BrokerName'] 415 | 416 | @property 417 | def id(self): 418 | return self.instance['BrokerId'] 419 | 420 | def terminate(self): 421 | self.client.delete_broker(BrokerId=self.id) 422 | -------------------------------------------------------------------------------- /aws/terminator/data_services.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import botocore.exceptions 4 | 5 | from . import DbTerminator, Terminator, get_tag_dict_from_tag_list 6 | 7 | 8 | class DmsSubnetGroup(DbTerminator): 9 | @staticmethod 10 | def create(credentials): 11 | def paginate_dms_subnet_groups(client): 12 | return client.get_paginator('describe_replication_subnet_groups').paginate().build_full_result()['ReplicationSubnetGroups'] 13 | 14 | return Terminator._create(credentials, DmsSubnetGroup, 'dms', paginate_dms_subnet_groups) 15 | 16 | @property 17 | def id(self): 18 | return self.instance['ReplicationSubnetGroupIdentifier'] 19 | 20 | @property 21 | def name(self): 22 | return self.instance['ReplicationSubnetGroupIdentifier'] 23 | 24 | def terminate(self): 25 | self.client.delete_replication_subnet_group(ReplicationSubnetGroupIdentifier=self.id) 26 | 27 | 28 | class RedshiftSubnetGroup(DbTerminator): 29 | @staticmethod 30 | def create(credentials): 31 | def paginate_redshift_subnet_groups(client): 32 | return client.get_paginator('describe_cluster_subnet_groups').paginate().build_full_result()['ClusterSubnetGroups'] 33 | 34 | return Terminator._create(credentials, RedshiftSubnetGroup, 'redshift', paginate_redshift_subnet_groups) 35 | 36 | @property 37 | def id(self): 38 | return self.instance['ClusterSubnetGroupName'] 39 | 40 | @property 41 | def name(self): 42 | return self.instance['ClusterSubnetGroupName'] 43 | 44 | def terminate(self): 45 | self.client.delete_cluster_subnet_group(ClusterSubnetGroupName=self.id) 46 | 47 | 48 | class Elasticache(Terminator): 49 | @staticmethod 50 | def create(credentials): 51 | 52 | def get_available_clusters(client): 53 | # describe_cache_clusters does not have a parameter to filter results 54 | # The key "CacheClusterCreateTime" does not exist while the cluster is being created. 55 | ignore_states = ('creating', 'deleting',) 56 | clusters = client.describe_cache_clusters()['CacheClusters'] 57 | return [cluster for cluster in clusters if cluster['CacheClusterStatus'] not in ignore_states] 58 | 59 | return Terminator._create(credentials, Elasticache, 'elasticache', get_available_clusters) 60 | 61 | @property 62 | def name(self): 63 | # Name is used like an ID 64 | return self.instance['CacheClusterId'] 65 | 66 | @property 67 | def id(self): 68 | return self.instance['CacheClusterId'] 69 | 70 | @property 71 | def created_time(self): 72 | return self.instance['CacheClusterCreateTime'] 73 | 74 | def terminate(self): 75 | self.client.delete_cache_cluster(CacheClusterId=self.id) 76 | 77 | 78 | class GlueConnection(Terminator): 79 | @staticmethod 80 | def create(credentials): 81 | return Terminator._create(credentials, GlueConnection, 'glue', lambda client: client.get_connections()['ConnectionList']) 82 | 83 | @property 84 | def id(self): 85 | return self.instance['Name'] 86 | 87 | @property 88 | def name(self): 89 | return self.instance['Name'] 90 | 91 | @property 92 | def created_time(self): 93 | return self.instance['CreationTime'] 94 | 95 | def terminate(self): 96 | self.client.delete_connection(ConnectionName=self.name) 97 | 98 | 99 | class GlueCrawler(Terminator): 100 | @staticmethod 101 | def create(credentials): 102 | return Terminator._create(credentials, GlueCrawler, 'glue', lambda client: client.get_crawlers()['Crawlers']) 103 | 104 | @property 105 | def id(self): 106 | return self.instance['Name'] 107 | 108 | @property 109 | def name(self): 110 | return self.instance['Name'] 111 | 112 | @property 113 | def created_time(self): 114 | return self.instance['CreationTime'] 115 | 116 | def terminate(self): 117 | self.client.delete_crawler(Name=self.name) 118 | 119 | 120 | class GlueJob(Terminator): 121 | @staticmethod 122 | def create(credentials): 123 | return Terminator._create(credentials, GlueJob, 'glue', lambda client: client.get_jobs()['Jobs']) 124 | 125 | @property 126 | def id(self): 127 | return self.instance['Name'] 128 | 129 | @property 130 | def name(self): 131 | return self.instance['Name'] 132 | 133 | @property 134 | def created_time(self): 135 | return self.instance['CreatedOn'] 136 | 137 | def terminate(self): 138 | self.client.delete_job(JobName=self.name) 139 | 140 | 141 | class Glacier(Terminator): 142 | @staticmethod 143 | def create(credentials): 144 | return Terminator._create(credentials, Glacier, 'glacier', lambda client: client.list_vaults()['VaultList']) 145 | 146 | @property 147 | def id(self): 148 | return self.instance['VaultARN'] 149 | 150 | @property 151 | def name(self): 152 | return self.instance['VaultName'] 153 | 154 | @property 155 | def created_time(self): 156 | return self.instance['CreationDate'] 157 | 158 | def terminate(self): 159 | self.client.delete_vault(vaultName=self.name) 160 | 161 | 162 | class RdsDbParameterGroup(DbTerminator): 163 | @staticmethod 164 | def create(credentials): 165 | return Terminator._create(credentials, RdsDbParameterGroup, 'rds', lambda client: client.describe_db_parameter_groups()['DBParameterGroups']) 166 | 167 | @property 168 | def id(self): 169 | return self.instance['DBParameterGroupArn'] 170 | 171 | @property 172 | def name(self): 173 | return self.instance['DBParameterGroupName'] 174 | 175 | @property 176 | def ignore(self): 177 | return self.name.startswith('default') 178 | 179 | def terminate(self): 180 | self.client.delete_db_parameter_group(DBParameterGroupName=self.name) 181 | 182 | 183 | class RdsDbClusterParameterGroup(DbTerminator): 184 | @staticmethod 185 | def create(credentials): 186 | return Terminator._create(credentials, RdsDbClusterParameterGroup, 'rds', 187 | lambda client: client.describe_db_cluster_parameter_groups()['DBClusterParameterGroups']) 188 | 189 | @property 190 | def id(self): 191 | return self.instance['DBClusterParameterGroupArn'] 192 | 193 | @property 194 | def name(self): 195 | return self.instance['DBClusterParameterGroupName'] 196 | 197 | @property 198 | def ignore(self): 199 | return self.name.startswith('default') 200 | 201 | def terminate(self): 202 | self.client.delete_db_cluster_parameter_group(DBClusterParameterGroupName=self.name) 203 | 204 | 205 | class RdsDbInstance(DbTerminator): 206 | @staticmethod 207 | def create(credentials): 208 | return Terminator._create(credentials, RdsDbInstance, 'rds', lambda client: client.describe_db_instances()['DBInstances']) 209 | 210 | @property 211 | def id(self): 212 | return self.instance['DBInstanceArn'] 213 | 214 | @property 215 | def name(self): 216 | return self.instance['DBInstanceIdentifier'] 217 | 218 | @property 219 | def age_limit(self): 220 | return datetime.timedelta(minutes=60) 221 | 222 | def terminate(self): 223 | try: 224 | self.client.modify_db_instance(DBInstanceIdentifier=self.name, BackupRetentionPeriod=0, DeletionProtection=False) 225 | except botocore.exceptions.ClientError as ex: 226 | # The instance can't be modifed when it's part of a cluster 227 | if ex.response['Error']['Code'] != 'InvalidParameterCombination': 228 | raise 229 | self.client.delete_db_instance(DBInstanceIdentifier=self.name, SkipFinalSnapshot=True) 230 | 231 | 232 | class RdsDbSnapshot(DbTerminator): 233 | @staticmethod 234 | def create(credentials): 235 | return Terminator._create(credentials, RdsDbSnapshot, 'rds', 236 | lambda client: client.describe_db_snapshots(SnapshotType='manual')['DBSnapshots']) 237 | 238 | @property 239 | def id(self): 240 | return self.instance['DBSnapshotArn'] 241 | 242 | @property 243 | def name(self): 244 | return self.instance['DBSnapshotIdentifier'] 245 | 246 | def terminate(self): 247 | self.client.delete_db_snapshot(DBSnapshotIdentifier=self.name) 248 | 249 | 250 | class RdsDbCluster(Terminator): 251 | @staticmethod 252 | def create(credentials): 253 | return Terminator._create(credentials, RdsDbCluster, 'rds', lambda client: client.describe_db_clusters()['DBClusters']) 254 | 255 | @property 256 | def id(self): 257 | return self.instance['DBClusterArn'] 258 | 259 | @property 260 | def name(self): 261 | return self.instance['DBClusterIdentifier'] 262 | 263 | @property 264 | def age_limit(self): 265 | return datetime.timedelta(minutes=60) 266 | 267 | @property 268 | def created_time(self): 269 | return self.instance['ClusterCreateTime'] 270 | 271 | def terminate(self): 272 | self.client.modify_db_cluster(DBClusterIdentifier=self.name, BackupRetentionPeriod=1, DeletionProtection=False) 273 | self.client.delete_db_cluster(DBClusterIdentifier=self.name, SkipFinalSnapshot=True) 274 | 275 | 276 | class RdsDbClusterSnapshot(Terminator): 277 | @staticmethod 278 | def create(credentials): 279 | return Terminator._create(credentials, RdsDbClusterSnapshot, 'rds', 280 | lambda client: client.describe_db_cluster_snapshots(SnapshotType='manual')['DBClusterSnapshots']) 281 | 282 | @property 283 | def id(self): 284 | return self.instance['DBClusterSnapshotArn'] 285 | 286 | @property 287 | def name(self): 288 | return self.instance['DBClusterSnapshotIdentifier'] 289 | 290 | @property 291 | def created_time(self): 292 | return self.instance['SnapshotCreateTime'] 293 | 294 | def terminate(self): 295 | self.client.delete_db_cluster_snapshot(DBClusterSnapshotIdentifier=self.name) 296 | 297 | 298 | class RedshiftCluster(Terminator): 299 | @staticmethod 300 | def create(credentials): 301 | 302 | def get_available_clusters(client): 303 | # describe_clusters does not have a parameter to filter results 304 | # The key "ClusterCreateTime" does not exist while the cluster is being created. 305 | ignore_states = ('creating', 'deleting',) 306 | clusters = client.describe_clusters()['Clusters'] 307 | return [cluster for cluster in clusters if cluster['ClusterStatus'] not in ignore_states] 308 | 309 | return Terminator._create(credentials, RedshiftCluster, 'redshift', get_available_clusters) 310 | 311 | @property 312 | def name(self): 313 | return get_tag_dict_from_tag_list(self.instance.get('Tags')).get('Name') 314 | 315 | @property 316 | def id(self): 317 | return self.instance['ClusterIdentifier'] 318 | 319 | @property 320 | def created_time(self): 321 | return self.instance['ClusterCreateTime'] 322 | 323 | def terminate(self): 324 | self.client.delete_cluster(ClusterIdentifier=self.id, SkipFinalClusterSnapshot=True) 325 | 326 | 327 | class RdsOptionGroup(DbTerminator): 328 | @staticmethod 329 | def create(credentials): 330 | return Terminator._create(credentials, RdsOptionGroup, 'rds', lambda client: client.describe_option_groups()['OptionGroupsList']) 331 | 332 | @property 333 | def id(self): 334 | return self.instance['OptionGroupArn'] 335 | 336 | @property 337 | def name(self): 338 | return self.instance['OptionGroupName'] 339 | 340 | @property 341 | def ignore(self): 342 | return self.name.startswith('default') 343 | 344 | def terminate(self): 345 | self.client.delete_option_group(OptionGroupName=self.name) 346 | 347 | 348 | class KafkaConfiguration(Terminator): 349 | @staticmethod 350 | def create(credentials): 351 | return Terminator._create(credentials, KafkaConfiguration, 'kafka', lambda client: client.list_configurations()['Configurations']) 352 | 353 | @property 354 | def id(self): 355 | return self.instance['Arn'] 356 | 357 | @property 358 | def name(self): 359 | return self.instance['Name'] 360 | 361 | @property 362 | def created_time(self): 363 | return self.instance['CreationTime'] 364 | 365 | @property 366 | def age_limit(self): 367 | return datetime.timedelta(minutes=60) 368 | 369 | def terminate(self): 370 | self.client.delete_configuration(Arn=self.id) 371 | 372 | 373 | class KafkaCluster(Terminator): 374 | @staticmethod 375 | def create(credentials): 376 | return Terminator._create(credentials, KafkaCluster, 'kafka', lambda client: client.list_clusters()['ClusterInfoList']) 377 | 378 | @property 379 | def id(self): 380 | return self.instance['ClusterArn'] 381 | 382 | @property 383 | def name(self): 384 | return self.instance['ClusterName'] 385 | 386 | @property 387 | def created_time(self): 388 | return self.instance['CreationTime'] 389 | 390 | @property 391 | def age_limit(self): 392 | return datetime.timedelta(minutes=60) 393 | 394 | def terminate(self): 395 | self.client.delete_cluster(ClusterArn=self.id) 396 | -------------------------------------------------------------------------------- /aws/terminator/networking.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import botocore 3 | from . import DbTerminator, Terminator, get_tag_dict_from_tag_list 4 | 5 | 6 | class Route53HostedZone(DbTerminator): 7 | @staticmethod 8 | def create(credentials): 9 | return Terminator._create(credentials, Route53HostedZone, 'route53', lambda client: client.list_hosted_zones()['HostedZones']) 10 | 11 | @property 12 | def id(self): 13 | return self.instance['Id'] 14 | 15 | @property 16 | def name(self): 17 | return self.instance['Name'] 18 | 19 | def _handle_ksk(self, ksk_list): 20 | for ksk in ksk_list: 21 | if ksk["Status"] == "ACTIVE": 22 | # Deactivate the Key Signing Request 23 | self.client.deactivate_key_signing_key(HostedZoneId=self.id, Name=ksk["Name"]) 24 | 25 | if ksk["Status"] == "DELETING": 26 | pass 27 | 28 | # Delete the Key Signing Request 29 | self.client.delete_key_signing_key(HostedZoneId=self.id, Name=ksk["Name"]) 30 | 31 | def terminate(self): 32 | # remove any record sets that the zone contains 33 | record_sets = self.client.list_resource_record_sets(HostedZoneId=self.id)['ResourceRecordSets'] 34 | remove_record_sets = [record_set for record_set in record_sets if record_set['Type'] not in ('SOA', 'NS')] 35 | if remove_record_sets: 36 | # Public hosted zones always contain an NS and SOA record, just try to remove the others 37 | self.client.change_resource_record_sets( 38 | HostedZoneId=self.id, 39 | ChangeBatch={ 40 | 'Comment': 'Remove record sets', 41 | 'Changes': [{ 42 | 'Action': 'DELETE', 43 | 'ResourceRecordSet': record_set 44 | } for record_set in remove_record_sets] 45 | } 46 | ) 47 | 48 | dnssec = self.client.get_dnssec(HostedZoneId=self.id) 49 | if dnssec["KeySigningKeys"] != []: 50 | if dnssec["Status"]["ServeSignature"] != "SIGNING": 51 | # Disable DNSSEC for the hosted zone 52 | self.client.disable_hosted_zone_dnssec(HostedZoneId=self.id) 53 | 54 | self._handle_ksk(dnssec["KeySigningKeys"]) 55 | 56 | self.client.delete_hosted_zone(Id=self.id) 57 | 58 | 59 | class Route53HealthCheck(DbTerminator): 60 | @staticmethod 61 | def create(credentials): 62 | return Terminator._create(credentials, Route53HealthCheck, 'route53', lambda client: client.list_health_checks()['HealthChecks']) 63 | 64 | @property 65 | def id(self): 66 | return self.instance['Id'] 67 | 68 | @property 69 | def name(self): 70 | return self.instance['Id'] 71 | 72 | def terminate(self): 73 | self.client.delete_health_check(HealthCheckId=self.id) 74 | 75 | 76 | class Ec2Eip(DbTerminator): 77 | @staticmethod 78 | def create(credentials): 79 | return Terminator._create(credentials, Ec2Eip, 'ec2', lambda client: client.describe_addresses()['Addresses']) 80 | 81 | @property 82 | def id(self): 83 | return self.instance['AllocationId'] 84 | 85 | @property 86 | def name(self): 87 | return self.instance['AllocationId'] 88 | 89 | def terminate(self): 90 | self.client.release_address(AllocationId=self.id) 91 | 92 | 93 | class Ec2CustomerGateway(DbTerminator): 94 | @staticmethod 95 | def create(credentials): 96 | return Terminator._create(credentials, Ec2CustomerGateway, 'ec2', lambda client: client.describe_customer_gateways()['CustomerGateways']) 97 | 98 | @property 99 | def age_limit(self): 100 | return datetime.timedelta(minutes=25) 101 | 102 | @property 103 | def id(self): 104 | return self.instance['CustomerGatewayId'] 105 | 106 | @property 107 | def name(self): 108 | return get_tag_dict_from_tag_list(self.instance.get('Tags')).get('Name') 109 | 110 | def terminate(self): 111 | self.client.delete_customer_gateway(CustomerGatewayId=self.id) 112 | 113 | 114 | class DhcpOptionsSet(DbTerminator): 115 | @staticmethod 116 | def create(credentials): 117 | return Terminator._create(credentials, DhcpOptionsSet, 'ec2', lambda client: client.describe_dhcp_options()['DhcpOptions']) 118 | 119 | @property 120 | def id(self): 121 | return self.instance['DhcpOptionsId'] 122 | 123 | @property 124 | def name(self): 125 | return self.instance['DhcpOptionsId'] 126 | 127 | @property 128 | def ignore(self): 129 | return self.default_vpc.get('DhcpOptionsId') == self.id 130 | 131 | def terminate(self): 132 | self.client.delete_dhcp_options(DhcpOptionsId=self.id) 133 | 134 | 135 | class Ec2Subnet(DbTerminator): 136 | @staticmethod 137 | def create(credentials): 138 | return Terminator._create(credentials, Ec2Subnet, 'ec2', lambda client: client.describe_subnets()['Subnets']) 139 | 140 | @property 141 | def age_limit(self): 142 | return datetime.timedelta(minutes=25) 143 | 144 | @property 145 | def id(self): 146 | return self.instance['SubnetId'] 147 | 148 | @property 149 | def name(self): 150 | return get_tag_dict_from_tag_list(self.instance.get('Tags')).get('Name') 151 | 152 | @property 153 | def ignore(self): 154 | return self.instance['DefaultForAz'] 155 | 156 | def terminate(self): 157 | self.client.delete_subnet(SubnetId=self.id) 158 | 159 | 160 | class Ec2InternetGateway(DbTerminator): 161 | def __init__(self, client, instance): 162 | self._ignore = None 163 | super().__init__(client, instance) 164 | 165 | @staticmethod 166 | def create(credentials): 167 | return Terminator._create(credentials, Ec2InternetGateway, 'ec2', lambda client: client.describe_internet_gateways()['InternetGateways']) 168 | 169 | @property 170 | def age_limit(self): 171 | return datetime.timedelta(minutes=25) 172 | 173 | @property 174 | def id(self): 175 | return self.instance['InternetGatewayId'] 176 | 177 | @property 178 | def name(self): 179 | return get_tag_dict_from_tag_list(self.instance.get('Tags')).get('Name') 180 | 181 | @property 182 | def ignore(self): 183 | if self._ignore is None: 184 | attachments = self._find_vpc_attachments() 185 | self._ignore = any(self.is_vpc_default(attachment_id) for attachment_id in attachments) 186 | return self._ignore 187 | 188 | def _find_vpc_attachments(self): 189 | return [attachment['VpcId'] for attachment in self.instance.get('Attachments', []) if attachment.get('VpcId')] 190 | 191 | def terminate(self): 192 | for attachment in self._find_vpc_attachments(): 193 | self.client.detach_internet_gateway(InternetGatewayId=self.id, VpcId=attachment) 194 | 195 | self.client.delete_internet_gateway(InternetGatewayId=self.id) 196 | 197 | 198 | class Ec2EgressInternetGateway(DbTerminator): 199 | @staticmethod 200 | def create(credentials): 201 | return Terminator._create(credentials, Ec2EgressInternetGateway, 'ec2', 202 | lambda client: client.describe_egress_only_internet_gateways()['EgressOnlyInternetGateways']) 203 | 204 | @property 205 | def id(self): 206 | return self.instance['EgressOnlyInternetGatewayId'] 207 | 208 | @property 209 | def name(self): 210 | return self.instance['EgressOnlyInternetGatewayId'] 211 | 212 | def terminate(self): 213 | self.client.delete_egress_only_internet_gateway(EgressOnlyInternetGatewayId=self.id) 214 | 215 | 216 | class Ec2NatGateway(DbTerminator): 217 | @staticmethod 218 | def create(credentials): 219 | return Terminator._create(credentials, Ec2NatGateway, 'ec2', lambda client: client.describe_nat_gateways()['NatGateways']) 220 | 221 | @property 222 | def id(self): 223 | return self.instance['NatGatewayId'] 224 | 225 | @property 226 | def name(self): 227 | return get_tag_dict_from_tag_list(self.instance.get('Tags')).get('Name') 228 | 229 | def terminate(self): 230 | self.client.delete_nat_gateway(NatGatewayId=self.id) 231 | 232 | 233 | class Ec2NetworkAcl(DbTerminator): 234 | @staticmethod 235 | def create(credentials): 236 | return Terminator._create(credentials, Ec2NetworkAcl, 'ec2', lambda client: client.describe_network_acls()['NetworkAcls']) 237 | 238 | @property 239 | def id(self): 240 | return self.instance['NetworkAclId'] 241 | 242 | @property 243 | def name(self): 244 | return get_tag_dict_from_tag_list(self.instance.get('Tags')).get('Name') 245 | 246 | @property 247 | def ignore(self): 248 | return self.instance['IsDefault'] 249 | 250 | def terminate(self): 251 | self.client.delete_network_acl(NetworkAclId=self.id) 252 | 253 | 254 | class Ec2Eni(DbTerminator): 255 | @staticmethod 256 | def create(credentials): 257 | return Terminator._create(credentials, Ec2Eni, 'ec2', lambda client: client.describe_network_interfaces()['NetworkInterfaces']) 258 | 259 | @property 260 | def age_limit(self): 261 | return datetime.timedelta(minutes=25) 262 | 263 | @property 264 | def id(self): 265 | return self.instance['NetworkInterfaceId'] 266 | 267 | @property 268 | def name(self): 269 | return get_tag_dict_from_tag_list(self.instance.get('Tags')).get('Name') 270 | 271 | def terminate(self): 272 | self.client.delete_network_interface(NetworkInterfaceId=self.id) 273 | 274 | 275 | class Ec2RouteTable(DbTerminator): 276 | @staticmethod 277 | def create(credentials): 278 | return Terminator._create(credentials, Ec2RouteTable, 'ec2', lambda client: client.describe_route_tables()['RouteTables']) 279 | 280 | @property 281 | def name(self): 282 | return get_tag_dict_from_tag_list(self.instance.get('Tags')).get('Name') 283 | 284 | @property 285 | def id(self): 286 | return self.instance['RouteTableId'] 287 | 288 | @property 289 | def ignore(self): 290 | # The main route table of a VPC cannot be deleted. 291 | # See: https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Route_Tables.html 292 | # They will be removed when the VPC is deleted. 293 | return any(association['Main'] for association in self.instance.get('Associations', [])) 294 | 295 | def terminate(self): 296 | for association in self.instance.get('Associations', []): 297 | self.client.disassociate_route_table(AssociationId=association['RouteTableAssociationId']) 298 | 299 | self.client.delete_route_table(RouteTableId=self.id) 300 | 301 | 302 | class Ec2VpcEndpoint(Terminator): 303 | @staticmethod 304 | def create(credentials): 305 | return Terminator._create(credentials, Ec2VpcEndpoint, 'ec2', lambda client: client.describe_vpc_endpoints()['VpcEndpoints']) 306 | 307 | @property 308 | def id(self): 309 | return self.instance['VpcEndpointId'] 310 | 311 | @property 312 | def name(self): 313 | return self.instance['ServiceName'] 314 | 315 | @property 316 | def created_time(self): 317 | return self.instance['CreationTimestamp'] 318 | 319 | def terminate(self): 320 | self.client.delete_vpc_endpoints(VpcEndpointIds=[self.id]) 321 | 322 | 323 | class Ec2Vpc(DbTerminator): 324 | @staticmethod 325 | def create(credentials): 326 | return Terminator._create(credentials, Ec2Vpc, 'ec2', lambda client: client.describe_vpcs()['Vpcs']) 327 | 328 | @property 329 | def age_limit(self): 330 | return datetime.timedelta(minutes=40) 331 | 332 | @property 333 | def id(self): 334 | return self.instance['VpcId'] 335 | 336 | @property 337 | def name(self): 338 | return get_tag_dict_from_tag_list(self.instance.get('Tags')).get('Name') 339 | 340 | @property 341 | def ignore(self): 342 | return self.instance['IsDefault'] 343 | 344 | def terminate(self): 345 | self.client.delete_vpc(VpcId=self.id) 346 | 347 | 348 | class Ec2VpnConnection(DbTerminator): 349 | @staticmethod 350 | def create(credentials): 351 | return Terminator._create(credentials, Ec2VpnConnection, 'ec2', lambda client: client.describe_vpn_connections()['VpnConnections']) 352 | 353 | @property 354 | def id(self): 355 | return self.instance['VpnConnectionId'] 356 | 357 | @property 358 | def name(self): 359 | return get_tag_dict_from_tag_list(self.instance.get('Tags')).get('Name') 360 | 361 | @property 362 | def ignore(self): 363 | # describe_vpn_connections will return results in deleting and deleted states. 364 | # Ignore connections in these current states. 365 | return self.instance.get('State') in ['deleting', 'deleted'] 366 | 367 | def terminate(self): 368 | self.client.delete_vpn_connection(VpnConnectionId=self.id) 369 | 370 | 371 | class Ec2VpnGateway(DbTerminator): 372 | @staticmethod 373 | def create(credentials): 374 | return Terminator._create(credentials, Ec2VpnGateway, 'ec2', lambda client: client.describe_vpn_gateways()['VpnGateways']) 375 | 376 | @property 377 | def id(self): 378 | return self.instance['VpnGatewayId'] 379 | 380 | @property 381 | def name(self): 382 | return get_tag_dict_from_tag_list(self.instance.get('Tags')).get('Name') 383 | 384 | def terminate(self): 385 | vpc_attachments = [attachment['VpcId'] for attachment in self.instance.get('VpcAttachments', []) if attachment['State'].startswith('attach')] 386 | for vpc_attachment in vpc_attachments: 387 | self.client.detach_vpn_gateway( 388 | VpcId=vpc_attachment, 389 | VpnGatewayId=self.id 390 | ) 391 | self.client.delete_vpn_gateway(VpnGatewayId=self.id) 392 | 393 | 394 | class Ec2VpcPeer(DbTerminator): 395 | @staticmethod 396 | def create(credentials): 397 | return Terminator._create(credentials, Ec2VpcPeer, 'ec2', lambda client: client.describe_vpc_peering_connections()['VpcPeeringConnections']) 398 | 399 | @property 400 | def id(self): 401 | return self.instance['VpcPeeringConnectionId'] 402 | 403 | @property 404 | def name(self): 405 | return get_tag_dict_from_tag_list(self.instance.get('Tags')).get('Name') 406 | 407 | @property 408 | def ignore(self): 409 | if self.instance.get('Status', {}).get('Code') in ['rejected', 'deleted']: 410 | return True 411 | return False 412 | 413 | def terminate(self): 414 | self.client.delete_vpc_peering_connection(VpcPeeringConnectionId=self.id) 415 | 416 | 417 | class Ec2SecurityGroup(DbTerminator): 418 | @staticmethod 419 | def create(credentials): 420 | return Terminator._create(credentials, Ec2SecurityGroup, 'ec2', lambda client: client.describe_security_groups()['SecurityGroups']) 421 | 422 | @property 423 | def age_limit(self): 424 | return datetime.timedelta(minutes=30) 425 | 426 | @property 427 | def id(self): 428 | return self.instance['GroupId'] 429 | 430 | @property 431 | def name(self): 432 | return self.instance['GroupName'] 433 | 434 | @property 435 | def ignore(self): 436 | return self.name == 'default' or self.name.startswith('default_elb_') 437 | 438 | def revoke_sg_rules(self): 439 | # Revoke Egress rules 440 | self.client.revoke_security_group_egress( 441 | GroupId=self.id, IpPermissions=self.instance.get("IpPermissionsEgress") 442 | ) 443 | 444 | # Revoke Ingress rules 445 | self.client.revoke_security_group_ingress( 446 | GroupId=self.id, IpPermissions=self.instance.get("IpPermissions") 447 | ) 448 | 449 | def terminate(self): 450 | try: 451 | self.client.delete_security_group(GroupId=self.id) 452 | except botocore.exceptions.ClientError as ex: 453 | if ex.response['Error']['Code'] == "DependencyViolation": 454 | self.revoke_sg_rules() 455 | 456 | 457 | class ApiGatewayRestApi(Terminator): 458 | @staticmethod 459 | def create(credentials): 460 | return Terminator._create(credentials, ApiGatewayRestApi, 'apigateway', lambda client: client.get_rest_apis()['items']) 461 | 462 | @property 463 | def id(self): 464 | return self.instance['id'] 465 | 466 | @property 467 | def name(self): 468 | return self.instance['name'] 469 | 470 | @property 471 | def created_time(self): 472 | return self.instance['createdDate'] 473 | 474 | def terminate(self): 475 | self.client.delete_rest_api(restApiId=self.id) 476 | 477 | 478 | class NetworkFirewall(DbTerminator): 479 | @staticmethod 480 | def create(credentials): 481 | return Terminator._create(credentials, NetworkFirewall, 'network-firewall', lambda client: client.list_firewalls()['Firewalls']) 482 | 483 | @property 484 | def id(self): 485 | return self.instance['FirewallArn'] 486 | 487 | @property 488 | def name(self): 489 | return self.instance['FirewallName'] 490 | 491 | def terminate(self): 492 | self.client.update_firewall_delete_protection(FirewallArn=self.id, DeleteProtection=False) 493 | self.client.delete_firewall(FirewallArn=self.id) 494 | 495 | 496 | class NetworkFirewallPolicy(DbTerminator): 497 | @staticmethod 498 | def create(credentials): 499 | return Terminator._create(credentials, NetworkFirewallPolicy, 'network-firewall', lambda client: client.list_firewall_policies()['FirewallPolicies']) 500 | 501 | @property 502 | def age_limit(self): 503 | # If there's a parent Firewall with the Policy attached, it can take 10 504 | # minutes for the Firewall to finish deleting 505 | return datetime.timedelta(minutes=30) 506 | 507 | @property 508 | def id(self): 509 | return self.instance['Arn'] 510 | 511 | @property 512 | def name(self): 513 | return self.instance['Name'] 514 | 515 | def terminate(self): 516 | self.client.delete_firewall_policy(FirewallPolicyArn=self.id) 517 | 518 | 519 | class NetworkFirewallRuleGroup(DbTerminator): 520 | @staticmethod 521 | def create(credentials): 522 | return Terminator._create(credentials, NetworkFirewallRuleGroup, 'network-firewall', lambda client: client.list_rule_groups()['RuleGroups']) 523 | 524 | @property 525 | def age_limit(self): 526 | return datetime.timedelta(minutes=35) 527 | 528 | @property 529 | def id(self): 530 | return self.instance['Arn'] 531 | 532 | @property 533 | def name(self): 534 | return self.instance['Name'] 535 | 536 | def terminate(self): 537 | self.client.delete_rule_group(RuleGroupArn=self.id) 538 | -------------------------------------------------------------------------------- /aws/terminator/paas.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | 3 | from . import DbTerminator, Terminator 4 | 5 | 6 | class LambdaEventSourceMapping(DbTerminator): 7 | @staticmethod 8 | def create(credentials): 9 | return Terminator._create(credentials, LambdaEventSourceMapping, 'lambda', lambda client: client.list_event_source_mappings()['EventSourceMappings']) 10 | 11 | @property 12 | def id(self): 13 | return self.instance['UUID'] 14 | 15 | @property 16 | def name(self): 17 | return self.id 18 | 19 | @property 20 | def ignore(self) -> bool: 21 | return self.instance['State'] in ('Creating', 'Enabling', 'Disabling', 'Updating', 'Deleting') 22 | 23 | def terminate(self): 24 | self.client.delete_event_source_mapping(UUID=self.id) 25 | 26 | 27 | class LambdaLayers(Terminator): 28 | @staticmethod 29 | def create(credentials): 30 | return Terminator._create(credentials, LambdaLayers, 'lambda', lambda client: client.list_layers()['Layers']) 31 | 32 | @property 33 | def id(self): 34 | return self.instance['LayerArn'] 35 | 36 | @property 37 | def name(self): 38 | return self.instance['LayerName'] 39 | 40 | @property 41 | def created_time(self): 42 | return datetime.strptime(self.instance['LatestMatchingVersion']['CreatedDate'], "%Y-%m-%dT%H:%M:%S.%f%z") 43 | 44 | def terminate(self): 45 | for version in self.client.list_layer_versions(LayerName=self.name)['LayerVersions']: 46 | self.client.delete_layer_version(LayerName=self.name, VersionNumber=version['Version']) 47 | 48 | 49 | class CloudFrontDistribution(Terminator): 50 | @staticmethod 51 | def create(credentials): 52 | def list_cloudfront_distributions(client): 53 | result = client.get_paginator('list_distributions').paginate().build_full_result() 54 | return result.get('DistributionList', {}).get('Items', []) 55 | 56 | return Terminator._create(credentials, CloudFrontDistribution, 'cloudfront', list_cloudfront_distributions) 57 | 58 | @property 59 | def created_time(self): 60 | return self.instance['LastModifiedTime'] 61 | 62 | @property 63 | def name(self): 64 | return self.instance['DomainName'] 65 | 66 | @property 67 | def Id(self): 68 | return self.instance['Id'] 69 | 70 | def terminate(self): 71 | 72 | distribution = self.client.get_distribution(Id=self.Id) 73 | ETag = distribution['ETag'] 74 | distribution = distribution['Distribution'] 75 | if distribution.get('Status') == "Deployed": 76 | if distribution['DistributionConfig']['Enabled']: 77 | # disable distribution 78 | distribution['DistributionConfig']['Enabled'] = False 79 | self.client.update_distribution(DistributionConfig=distribution['DistributionConfig'], Id=self.Id, IfMatch=ETag) 80 | else: 81 | # delete distribution 82 | self.client.delete_distribution(Id=self.Id, IfMatch=ETag) 83 | 84 | 85 | class CloudFrontStreamingDistribution(Terminator): 86 | @staticmethod 87 | def create(credentials): 88 | def list_cloudfront_streaming_distributions(client): 89 | result = client.get_paginator('list_streaming_distributions').paginate().build_full_result() 90 | return result.get('StreamingDistributionList', {}).get('Items', []) 91 | 92 | return Terminator._create(credentials, CloudFrontStreamingDistribution, 'cloudfront', list_cloudfront_streaming_distributions) 93 | 94 | @property 95 | def created_time(self): 96 | return self.instance['LastModifiedTime'] 97 | 98 | @property 99 | def name(self): 100 | return self.instance['DomainName'] 101 | 102 | @property 103 | def Id(self): 104 | return self.instance['Id'] 105 | 106 | def terminate(self): 107 | streaming_distribution = self.client.get_streaming_distribution(Id=self.Id) 108 | ETag = streaming_distribution['ETag'] 109 | streaming_distribution = streaming_distribution['StreamingDistribution'] 110 | if streaming_distribution.get('Status') == "Deployed": 111 | if streaming_distribution['StreamingDistributionConfig']['Enabled']: 112 | # disable streaming distribution 113 | streaming_distribution['StreamingDistributionConfig']['Enabled'] = False 114 | self.client.update_streaming_distribution(StreamingDistributionConfig=streaming_distribution['StreamingDistributionConfig'], 115 | Id=self.Id, 116 | IfMatch=ETag) 117 | else: 118 | # delete streaming distribution 119 | self.client.delete_streaming_distribution(Id=self.Id, IfMatch=ETag) 120 | 121 | 122 | class CloudFrontOriginAccessIdentity(DbTerminator): 123 | @staticmethod 124 | def create(credentials): 125 | def list_cloud_front_origin_access_identities(client): 126 | identities = [] 127 | result = client.get_paginator('list_cloud_front_origin_access_identities').paginate().build_full_result() 128 | for identity in result.get('CloudFrontOriginAccessIdentityList', {}).get('Items', []): 129 | identities.append(client.get_cloud_front_origin_access_identity(Id=identity['Id'])) 130 | return identities 131 | 132 | return Terminator._create(credentials, CloudFrontOriginAccessIdentity, 'cloudfront', list_cloud_front_origin_access_identities) 133 | 134 | @property 135 | def id(self): 136 | return self.instance['ETag'] 137 | 138 | @property 139 | def name(self): 140 | return self.instance['CloudFrontOriginAccessIdentity']['Id'] 141 | 142 | def terminate(self): 143 | self.client.delete_cloud_front_origin_access_identity(Id=self.name, IfMatch=self.id) 144 | 145 | 146 | class CloudFrontCachePolicy(DbTerminator): 147 | @staticmethod 148 | def create(credentials): 149 | def list_cloud_front_cache_policies(client): 150 | identities = [] 151 | result = client.list_cache_policies( 152 | # Only retrieve the custom policies 153 | Type='custom' 154 | ) 155 | for identity in result.get('CachePolicyList', {}).get('Items', []): 156 | identities.append(client.get_cache_policy(Id=identity['CachePolicy']['Id'])) 157 | return identities 158 | 159 | return Terminator._create(credentials, CloudFrontCachePolicy, 'cloudfront', list_cloud_front_cache_policies) 160 | 161 | @property 162 | def id(self): 163 | return self.instance['ETag'] 164 | 165 | @property 166 | def name(self): 167 | return self.instance['CachePolicy']['Id'] 168 | 169 | @property 170 | def age_limit(self): 171 | return timedelta(minutes=30) 172 | 173 | def terminate(self): 174 | self.client.delete_cache_policy(Id=self.name, IfMatch=self.id) 175 | 176 | 177 | class CloudFrontOriginRequestPolicy(DbTerminator): 178 | @staticmethod 179 | def create(credentials): 180 | def list_cloud_front_origin_request_policies(client): 181 | identities = [] 182 | result = client.list_origin_request_policies( 183 | # Only retrieve the custom policies 184 | Type='custom' 185 | ) 186 | for identity in result.get('OriginRequestPolicyList', {}).get('Items', []): 187 | identities.append(client.get_origin_request_policy(Id=identity['OriginRequestPolicy']['Id'])) 188 | return identities 189 | 190 | return Terminator._create(credentials, CloudFrontOriginRequestPolicy, 'cloudfront', list_cloud_front_origin_request_policies) 191 | 192 | @property 193 | def id(self): 194 | return self.instance['ETag'] 195 | 196 | @property 197 | def name(self): 198 | return self.instance['OriginRequestPolicy']['Id'] 199 | 200 | @property 201 | def age_limit(self): 202 | return timedelta(minutes=30) 203 | 204 | def terminate(self): 205 | self.client.delete_origin_request_policy(Id=self.name, IfMatch=self.id) 206 | 207 | 208 | class Ecs(DbTerminator): 209 | @property 210 | def age_limit(self): 211 | return timedelta(minutes=20) 212 | 213 | @property 214 | def name(self): 215 | return self.instance['clusterName'] 216 | 217 | @staticmethod 218 | def create(credentials): 219 | def _paginate_cluster_results(client): 220 | names = client.get_paginator('list_clusters').paginate( 221 | PaginationConfig={ 222 | 'PageSize': 100, 223 | } 224 | ).build_full_result()['clusterArns'] 225 | 226 | if not names: 227 | return [] 228 | 229 | return client.describe_clusters(clusters=names)['clusters'] 230 | 231 | return Terminator._create(credentials, Ecs, 'ecs', _paginate_cluster_results) 232 | 233 | def terminate(self): 234 | def _paginate_task_results(container_instance=None): 235 | params = { 236 | 'cluster': self.name, 237 | 'PaginationConfig': { 238 | 'PageSize': 100, 239 | } 240 | } 241 | 242 | if container_instance: 243 | params['containerInstance'] = container_instance 244 | 245 | names = self.client.get_paginator('list_tasks').paginate( 246 | **params 247 | ).build_full_result()['taskArns'] 248 | 249 | return [] if not names else names 250 | 251 | def _paginate_task_definition_results(): 252 | names = self.client.get_paginator('list_task_definitions').paginate( 253 | PaginationConfig={ 254 | 'PageSize': 100, 255 | } 256 | ).build_full_result()['taskDefinitionArns'] 257 | 258 | return [] if not names else names 259 | 260 | def _paginate_container_instance_results(): 261 | names = self.client.get_paginator('list_container_instances').paginate( 262 | cluster=self.name, 263 | PaginationConfig={ 264 | 'PageSize': 100, 265 | } 266 | ).build_full_result()['containerInstanceArns'] 267 | 268 | return [] if not names else names 269 | 270 | def _paginate_service_results(): 271 | names = self.client.get_paginator('list_services').paginate( 272 | cluster=self.name, 273 | PaginationConfig={ 274 | 'PageSize': 100, 275 | } 276 | ).build_full_result()['serviceArns'] 277 | 278 | return [] if not names else names 279 | 280 | # If there are running services, delete them first 281 | services = _paginate_service_results() 282 | for each in services: 283 | self.client.delete_service(cluster=self.name, service=each, force=True) 284 | 285 | # Deregister container instances and stop any running task 286 | container_instances = _paginate_container_instance_results() 287 | for each in container_instances: 288 | self.client.deregister_container_instance(containerInstance=each['containerInstanceArn'], force=True) 289 | 290 | # Deregister task definitions 291 | task_definitions = _paginate_task_definition_results() 292 | for each in task_definitions: 293 | self.client.deregister_task_definition(taskDefinition=each) 294 | 295 | # Stop all the tasks 296 | tasks = _paginate_task_results() 297 | for each in tasks: 298 | self.client.stop_task(cluster=self.name, task=each) 299 | 300 | # Delete cluster 301 | try: 302 | self.client.delete_cluster(cluster=self.name) 303 | except (self.client.exceptions.ClusterContainsServicesException, self.client.exceptions.ClusterContainsTasksException): 304 | pass 305 | 306 | 307 | class EcsCluster(DbTerminator): 308 | @property 309 | def age_limit(self): 310 | return timedelta(minutes=30) 311 | 312 | @property 313 | def name(self): 314 | return self.instance['clusterName'] 315 | 316 | @staticmethod 317 | def create(credentials): 318 | def _paginate_cluster_results(client): 319 | names = client.get_paginator('list_clusters').paginate( 320 | PaginationConfig={ 321 | 'PageSize': 100, 322 | } 323 | ).build_full_result()['clusterArns'] 324 | 325 | if not names: 326 | return [] 327 | 328 | return client.describe_clusters(clusters=names)['clusters'] 329 | 330 | return Terminator._create(credentials, EcsCluster, 'ecs', _paginate_cluster_results) 331 | 332 | def terminate(self): 333 | self.client.delete_cluster(cluster=self.name) 334 | -------------------------------------------------------------------------------- /aws/terminator/security_services.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import botocore 4 | import botocore.exceptions 5 | 6 | from . import DbTerminator, Terminator 7 | 8 | 9 | class IamRole(Terminator): 10 | @staticmethod 11 | def create(credentials): 12 | return Terminator._create(credentials, IamRole, 'iam', lambda client: client.list_roles()['Roles']) 13 | 14 | @property 15 | def id(self): 16 | return self.instance['RoleId'] 17 | 18 | @property 19 | def name(self): 20 | return self.instance['RoleName'] 21 | 22 | @property 23 | def ignore(self): 24 | return not self.name.startswith('ansible-test') 25 | 26 | @property 27 | def created_time(self): 28 | return self.instance['CreateDate'] 29 | 30 | def terminate(self): 31 | try: 32 | self.client.delete_role(RoleName=self.name) 33 | return 34 | except botocore.exceptions.ClientError as ex: 35 | if ex.response['Error']['Code'] != 'DeleteConflict': 36 | raise 37 | 38 | for policy in self.client.list_attached_role_policies(RoleName=self.name)['AttachedPolicies']: 39 | self.client.detach_role_policy(RoleName=self.name, PolicyArn=policy['PolicyArn']) 40 | for policy in self.client.list_role_policies(RoleName=self.name)['PolicyNames']: 41 | self.client.delete_role_policy(RoleName=self.name, PolicyName=policy) 42 | 43 | self.client.delete_role(RoleName=self.name) 44 | 45 | 46 | class IamInstanceProfile(Terminator): 47 | @staticmethod 48 | def create(credentials): 49 | return Terminator._create(credentials, IamInstanceProfile, 'iam', lambda client: client.list_instance_profiles()['InstanceProfiles']) 50 | 51 | @property 52 | def id(self): 53 | return self.instance['InstanceProfileId'] 54 | 55 | @property 56 | def name(self): 57 | return self.instance['InstanceProfileName'] 58 | 59 | @property 60 | def ignore(self): 61 | return not self.name.startswith('ansible-test-') 62 | 63 | @property 64 | def created_time(self): 65 | return self.instance['CreateDate'] 66 | 67 | def terminate(self): 68 | for role in self.instance['Roles']: 69 | self.client.remove_role_from_instance_profile(InstanceProfileName=self.name, RoleName=role['RoleName']) 70 | 71 | self.client.delete_instance_profile(InstanceProfileName=self.name) 72 | 73 | 74 | class IamServerCertificate(Terminator): 75 | @staticmethod 76 | def create(credentials): 77 | return Terminator._create(credentials, IamServerCertificate, 'iam', lambda client: client.list_server_certificates()['ServerCertificateMetadataList']) 78 | 79 | @property 80 | def id(self): 81 | return self.instance['ServerCertificateId'] 82 | 83 | @property 84 | def name(self): 85 | return self.instance['ServerCertificateName'] 86 | 87 | @property 88 | def ignore(self): 89 | return not self.name.startswith('ansible-test-') 90 | 91 | @property 92 | def created_time(self): 93 | return self.instance['UploadDate'] 94 | 95 | def terminate(self): 96 | self.client.delete_server_certificate(ServerCertificateName=self.name) 97 | 98 | 99 | class ACMCertificate(DbTerminator): 100 | # ACM provides a created time, but there are cases where describe_certificate can fail 101 | # We need to be able to delete anyway, so use DbTerminator 102 | # https://github.com/ansible/ansible/issues/67788 103 | @staticmethod 104 | def create(credentials): 105 | return Terminator._create( 106 | credentials, ACMCertificate, 'acm', 107 | lambda client: client.get_paginator('list_certificates').paginate().build_full_result()['CertificateSummaryList'] 108 | ) 109 | 110 | @property 111 | def id(self): 112 | return self.instance['CertificateArn'] 113 | 114 | @property 115 | def name(self): 116 | return self.instance['CertificateArn'] 117 | 118 | def terminate(self): 119 | self.client.delete_certificate(CertificateArn=self.id) 120 | 121 | 122 | class IAMSamlProvider(Terminator): 123 | @staticmethod 124 | def create(credentials): 125 | return Terminator._create( 126 | credentials, IAMSamlProvider, 'iam', 127 | lambda client: client.list_saml_providers()['SAMLProviderList'] 128 | ) 129 | 130 | @property 131 | def id(self): 132 | return self.instance['Arn'] 133 | 134 | @property 135 | def name(self): 136 | return self.instance['Arn'].split('/')[-1] 137 | 138 | @property 139 | def ignore(self): 140 | return not self.name.startswith('ansible-test-') 141 | 142 | @property 143 | def created_time(self): 144 | return self.instance['CreateDate'] 145 | 146 | def terminate(self): 147 | self.client.delete_saml_provider(SAMLProviderArn=self.id) 148 | 149 | 150 | class KMSKey(Terminator): 151 | @staticmethod 152 | def create(credentials): 153 | def get_paginated_keys(client): 154 | return client.get_paginator('list_keys').paginate().build_full_result()['Keys'] 155 | 156 | def get_key_details(client, key): 157 | metadata = client.describe_key(KeyId=key['KeyId'])['KeyMetadata'] 158 | _aliases = client.list_aliases(KeyId=key['KeyId'])['Aliases'] 159 | aliases = [] 160 | for alias in _aliases: 161 | aliases.append(alias['AliasName']) 162 | metadata['Aliases'] = aliases 163 | return metadata 164 | 165 | def get_detailed_keys(client): 166 | detailed_keys = [] 167 | for key in get_paginated_keys(client): 168 | metadata = get_key_details(client, key) 169 | if metadata: 170 | detailed_keys.append(metadata) 171 | return detailed_keys 172 | 173 | return Terminator._create(credentials, KMSKey, 'kms', get_detailed_keys) 174 | 175 | @property 176 | def ignore(self): 177 | # The key is already in a 'pending deletion' state, and doesn't need 178 | # anything more done to it. 179 | if self.instance['KeyState'] == 'PendingDeletion': 180 | return True 181 | # Don't try deleting the AWS managed keys (they're not charged for) 182 | for alias in self.instance['Aliases']: 183 | if alias.startswith('alias/aws/'): 184 | return True 185 | return False 186 | 187 | @property 188 | def created_time(self): 189 | return self.instance['CreationDate'] 190 | 191 | @property 192 | def id(self): 193 | return self.instance['KeyId'] 194 | 195 | @property 196 | def name(self): 197 | return self.instance['Aliases'] 198 | 199 | def terminate(self): 200 | self.client.schedule_key_deletion(KeyId=self.id, PendingWindowInDays=7) 201 | 202 | 203 | class Secret(Terminator): 204 | @staticmethod 205 | def create(credentials): 206 | return Terminator._create(credentials, Secret, 'secretsmanager', lambda client: client.list_secrets()['SecretList']) 207 | 208 | @property 209 | def id(self): 210 | return self.instance['ARN'] 211 | 212 | @property 213 | def name(self): 214 | return self.instance['Name'] 215 | 216 | @property 217 | def ignore(self): 218 | return not self.name.startswith('ansible-test') 219 | 220 | @property 221 | def created_time(self): 222 | return self.instance['CreatedDate'] 223 | 224 | @property 225 | def age_limit(self): 226 | return datetime.timedelta(minutes=30) 227 | 228 | def terminate(self): 229 | try: 230 | self.client.delete_secret(SecretId=self.name, RecoveryWindowInDays=7) 231 | except botocore.exceptions.ClientError as ex: 232 | if ex.response['Error']['Code'] == "InvalidParameterException": 233 | region_list = [] 234 | for region in self.client.describe_secret(SecretId=self.name).get('ReplicationStatus', ""): 235 | region_list.append(region.get('Region', "")) 236 | self.client.remove_regions_from_replication(SecretId=self.name, RemoveReplicaRegions=region_list) 237 | self.client.delete_secret(SecretId=self.name, RecoveryWindowInDays=7) 238 | -------------------------------------------------------------------------------- /aws/terminator/storage_services.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import botocore 4 | import botocore.exceptions 5 | 6 | from . import DbTerminator, Terminator, get_account_id 7 | 8 | 9 | class S3Bucket(Terminator): 10 | @staticmethod 11 | def create(credentials): 12 | return Terminator._create(credentials, S3Bucket, 's3', lambda client: client.list_buckets()['Buckets']) 13 | 14 | @property 15 | def name(self): 16 | return self.instance['Name'] 17 | 18 | @property 19 | def ignore(self): 20 | # Bucket encryption takes up to 24 hours to be enabled, so we use a persistent bucket 21 | # We'll empty the bucket contents in SSMBucketObjects 22 | return self.instance['Name'] == 'ssm-encrypted-test-bucket' 23 | 24 | @property 25 | def created_time(self): 26 | return self.instance['CreationDate'] 27 | 28 | def terminate(self): 29 | def _paginated_versions_list(bucket): 30 | paginator = self.client.get_paginator("list_object_versions") 31 | for page in paginator.paginate(Bucket=bucket): 32 | # We have to merge the Versions and DeleteMarker lists here, 33 | # as DeleteMarkers can still prevent a bucket deletion 34 | yield [ 35 | {"Key": data["Key"], "VersionId": data["VersionId"]} for data in (page.get("Versions", []) + page.get("DeleteMarkers", [])) 36 | ] 37 | 38 | try: 39 | self.client.delete_bucket(Bucket=self.name) 40 | except botocore.exceptions.ClientError as ex: 41 | if ex.response['Error']['Code'] == 'NoSuchBucket': 42 | return 43 | if ex.response['Error']['Code'] == 'BucketNotEmpty': 44 | for object_versions in _paginated_versions_list(bucket=self.name): 45 | self.client.delete_objects( 46 | Bucket=self.name, 47 | Delete={ 48 | "Objects": object_versions, 49 | "Quiet": True, 50 | } 51 | ) 52 | 53 | self.client.delete_bucket(Bucket=self.name) 54 | 55 | 56 | class SSMBucketObjects(Terminator): 57 | # We maintain a persistent encrypted bucket for the commmunity.aws SSM connection plugin. 58 | # Ensure it is kept clean of objects from past test runs. 59 | @staticmethod 60 | def create(credentials): 61 | def paginate_objects(client): 62 | list_bucket_objects_result = client.get_paginator('list_objects_v2').paginate(Bucket='ssm-encrypted-test-bucket').build_full_result() 63 | bucket_contents = {} 64 | if list_bucket_objects_result.get('Contents'): 65 | bucket_contents = list_bucket_objects_result['Contents'] 66 | return bucket_contents 67 | 68 | return Terminator._create(credentials, SSMBucketObjects, 's3', paginate_objects) 69 | 70 | @property 71 | def created_time(self): 72 | return self.instance['LastModified'] 73 | 74 | @property 75 | def name(self): 76 | return self.instance['Key'] 77 | 78 | def terminate(self): 79 | self.client.delete_object(Bucket='ssm-encrypted-test-bucket', Key=self.name) 80 | 81 | 82 | class S3AccessPoint(Terminator): 83 | _account_id = None 84 | 85 | @staticmethod 86 | def create(credentials): 87 | account = get_account_id(credentials) 88 | 89 | def list_access_points(client): 90 | results = [] 91 | access_points = client.list_access_points(AccountId=account).get("AccessPointList", []) 92 | for ap in access_points: 93 | results.append(client.get_access_point(AccountId=account, Name=ap['Name'])) 94 | return results 95 | terminators = Terminator._create(credentials, S3AccessPoint, 's3control', list_access_points) 96 | for terminator in terminators: 97 | terminator._account_id = account 98 | return terminators 99 | 100 | @property 101 | def name(self): 102 | return self.instance['Name'] 103 | 104 | @property 105 | def created_time(self): 106 | return self.instance['CreationDate'] 107 | 108 | def terminate(self): 109 | self.client.delete_access_point(AccountId=self._account_id, Name=self.name) 110 | 111 | 112 | class S3AccessPointForObjectLambda(Terminator): 113 | _account_id = None 114 | 115 | @staticmethod 116 | def create(credentials): 117 | account = get_account_id(credentials) 118 | 119 | def list_access_points(client): 120 | results = [] 121 | access_points = client.list_access_points_for_object_lambda(AccountId=account).get("ObjectLambdaAccessPointList", []) 122 | for ap in access_points: 123 | results.append(client.get_access_point_for_object_lambda(AccountId=account, Name=ap['Name'])) 124 | return results 125 | terminators = Terminator._create(credentials, S3AccessPointForObjectLambda, 's3control', list_access_points) 126 | for terminator in terminators: 127 | terminator._account_id = account 128 | return terminators 129 | 130 | @property 131 | def name(self): 132 | return self.instance['Name'] 133 | 134 | @property 135 | def created_time(self): 136 | return self.instance['CreationDate'] 137 | 138 | def terminate(self): 139 | self.client.delete_access_point_for_object_lambda(AccountId=self._account_id, Name=self.name) 140 | 141 | 142 | class BackupPlan(Terminator): 143 | @staticmethod 144 | def create(credentials): 145 | def paginate_plans(client): 146 | list_backup_plans_result = ( 147 | client.get_paginator("list_backup_plans").paginate().build_full_result() 148 | ) 149 | return list_backup_plans_result["BackupPlansList"] 150 | 151 | return Terminator._create(credentials, BackupPlan, "backup", paginate_plans) 152 | 153 | @property 154 | def id(self): 155 | return self.instance["BackupPlanId"] 156 | 157 | @property 158 | def name(self): 159 | return self.instance["BackupPlanName"] 160 | 161 | @property 162 | def created_time(self): 163 | return self.instance["CreationDate"] 164 | 165 | def terminate(self): 166 | self.client.delete_backup_plan(BackupPlanId=self.id) 167 | 168 | 169 | class BackupVault(Terminator): 170 | @staticmethod 171 | def create(credentials): 172 | def paginate_vaults(client): 173 | list_backup_vaults_result = ( 174 | client.get_paginator("list_backup_vaults") 175 | .paginate() 176 | .build_full_result() 177 | ) 178 | return list_backup_vaults_result["BackupVaultList"] 179 | 180 | return Terminator._create(credentials, BackupVault, "backup", paginate_vaults) 181 | 182 | @property 183 | def name(self): 184 | return self.instance["BackupVaultName"] 185 | 186 | @property 187 | def created_time(self): 188 | return self.instance["CreationDate"] 189 | 190 | def terminate(self): 191 | self.client.delete_backup_vault(BackupVaultName=self.name) 192 | 193 | 194 | class BackupSelection(Terminator): 195 | @staticmethod 196 | def create(credentials): 197 | def _build_backup_selections(client): 198 | results = [] 199 | # Get AWS Backup plans 200 | backup_plans = ( 201 | client.get_paginator("list_backup_plans").paginate().build_full_result() 202 | )["BackupPlansList"] 203 | for plan in backup_plans: 204 | results.extend(( 205 | client.get_paginator("list_backup_selections") 206 | .paginate(BackupPlanId=plan["BackupPlanId"]) 207 | .build_full_result() 208 | )["BackupSelectionsList"]) 209 | return results 210 | return Terminator._create(credentials, BackupSelection, "backup", _build_backup_selections) 211 | 212 | @property 213 | def name(self): 214 | return self.instance["SelectionName"] 215 | 216 | @property 217 | def plan_id(self): 218 | return self.instance["BackupPlanId"] 219 | 220 | @property 221 | def id(self): 222 | return self.instance["SelectionId"] 223 | 224 | @property 225 | def created_time(self): 226 | return self.instance["CreationDate"] 227 | 228 | def terminate(self): 229 | self.client.delete_backup_selection(BackupPlanId=self.plan_id, SelectionId=self.id) 230 | 231 | 232 | class MemoryDBClusters(DbTerminator): 233 | @staticmethod 234 | def create(credentials): 235 | def get_available_clusters(client): 236 | # describe_clusters does not have a parameter to filter results 237 | ignore_states = ('creating', 'deleting', 'updating') 238 | clusters = client.describe_clusters()['Clusters'] 239 | return [cluster for cluster in clusters if cluster['Status'] not in ignore_states] 240 | return Terminator._create(credentials, MemoryDBClusters, 'memorydb', get_available_clusters) 241 | 242 | @property 243 | def id(self): 244 | return self.instance["ARN"] 245 | 246 | @property 247 | def name(self): 248 | return self.instance["Name"] 249 | 250 | def terminate(self): 251 | self.client.delete_cluster(ClusterName=self.name) 252 | 253 | 254 | class MemoryDBACLs(DbTerminator): 255 | @staticmethod 256 | def create(credentials): 257 | return Terminator._create(credentials, MemoryDBACLs, 'memorydb', lambda client: client.describe_acls()['ACLs']) 258 | 259 | @property 260 | def id(self): 261 | return self.instance["ARN"] 262 | 263 | @property 264 | def name(self): 265 | return self.instance["Name"] 266 | 267 | @property 268 | def ignore(self): 269 | return self.name == "open-access" 270 | 271 | @property 272 | def age_limit(self): 273 | return datetime.timedelta(minutes=40) 274 | 275 | def terminate(self): 276 | self.client.delete_acl(ACLName=self.name) 277 | 278 | 279 | class MemoryDBParameterGroups(DbTerminator): 280 | @staticmethod 281 | def create(credentials): 282 | return Terminator._create(credentials, MemoryDBParameterGroups, 'memorydb', lambda client: client.describe_parameter_groups()['ParameterGroups']) 283 | 284 | @property 285 | def id(self): 286 | return self.instance["ARN"] 287 | 288 | @property 289 | def name(self): 290 | return self.instance["Name"] 291 | 292 | @property 293 | def ignore(self): 294 | return self.name.startswith('default') 295 | 296 | @property 297 | def age_limit(self): 298 | return datetime.timedelta(minutes=40) 299 | 300 | def terminate(self): 301 | self.client.delete_parameter_group(ParameterGroupName=self.name) 302 | 303 | 304 | class MemoryDBSubnetGroups(DbTerminator): 305 | @staticmethod 306 | def create(credentials): 307 | return Terminator._create(credentials, MemoryDBSubnetGroups, 'memorydb', lambda client: client.describe_subnet_groups()['SubnetGroups']) 308 | 309 | @property 310 | def id(self): 311 | return self.instance["ARN"] 312 | 313 | @property 314 | def name(self): 315 | return self.instance["Name"] 316 | 317 | @property 318 | def age_limit(self): 319 | return datetime.timedelta(minutes=40) 320 | 321 | def terminate(self): 322 | self.client.delete_subnet_group(SubnetGroupName=self.name) 323 | 324 | 325 | class MemoryDBUsers(DbTerminator): 326 | @staticmethod 327 | def create(credentials): 328 | return Terminator._create(credentials, MemoryDBUsers, 'memorydb', lambda client: client.describe_users()['Users']) 329 | 330 | @property 331 | def id(self): 332 | return self.instance["ARN"] 333 | 334 | @property 335 | def name(self): 336 | return self.instance["Name"] 337 | 338 | @property 339 | def ignore(self): 340 | return self.name == "default" 341 | 342 | @property 343 | def age_limit(self): 344 | return datetime.timedelta(minutes=40) 345 | 346 | def terminate(self): 347 | self.client.delete_user(UserName=self.name) 348 | 349 | 350 | class MemoryDBSnapshots(Terminator): 351 | @staticmethod 352 | def create(credentials): 353 | return Terminator._create(credentials, MemoryDBSnapshots, 'memorydb', lambda client: client.describe_snapshots()['Snapshots']) 354 | 355 | @property 356 | def id(self): 357 | return self.instance["ARN"] 358 | 359 | @property 360 | def name(self): 361 | return self.instance["Name"] 362 | 363 | @property 364 | def created_time(self): 365 | return self.instance['ClusterConfiguration']['Shards']['SnapshotCreationTime'] 366 | 367 | def terminate(self): 368 | self.client.delete_snapshot(SnapshotName=self.name) 369 | -------------------------------------------------------------------------------- /aws/terminator_lambda.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from terminator import ( 5 | cleanup, 6 | logger, 7 | ) 8 | 9 | 10 | # noinspection PyUnusedLocal 11 | def lambda_handler(event, context): 12 | # pylint: disable=unused-argument 13 | logger.setLevel(logging.INFO) 14 | 15 | arn = context.invoked_function_arn.split(':') 16 | 17 | if len(arn) == 7: 18 | arn.append('prod') # hack to set the stage for testing in the lambda console 19 | 20 | if len(arn) != 8 or arn[5] != 'function' or arn[6] != context.function_name: 21 | raise Exception(f'error: unexpected arn: {arn}') 22 | 23 | stage = arn[7] 24 | 25 | api_name = os.environ['API_NAME'] 26 | test_account_id = os.environ['TEST_ACCOUNT_ID'] 27 | 28 | cleanup(stage, check=False, force=False, api_name=api_name, test_account_id=test_account_id) 29 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - main 3 | 4 | pr: 5 | - main 6 | 7 | pool: 8 | vmImage: 'ubuntu-22.04' 9 | 10 | steps: 11 | - task: UsePythonVersion@0 12 | inputs: 13 | versionSpec: '3.9' 14 | displayName: Install Python 3.9 15 | - script: pip install --user tox 16 | displayName: Install tox 17 | - script: tox --notest 18 | displayName: Install test requirements 19 | - script: tox 20 | displayName: Run tests 21 | -------------------------------------------------------------------------------- /constraints.txt: -------------------------------------------------------------------------------- 1 | # freeze pylint and its requirements for consistent test results 2 | ansible-core==2.13.5 3 | astroid==2.12.12 4 | cffi==1.15.1 5 | cryptography==38.0.3 6 | dill==0.3.6 7 | isort==5.10.1 8 | Jinja2==3.1.2 9 | lazy-object-proxy==1.8.0 10 | MarkupSafe==2.1.1 11 | mccabe==0.7.0 12 | packaging==21.3 13 | pathspec==0.10.1 14 | platformdirs==2.5.2 15 | pycodestyle==2.9.1 16 | pycparser==2.21 17 | pylint==2.15.5 18 | pyparsing==3.0.9 19 | PyYAML==6.0 20 | resolvelib==0.8.1 21 | tomli==2.0.1 22 | tomlkit==0.11.6 23 | typing_extensions==4.4.0 24 | wrapt==1.14.1 25 | yamllint==1.28.0 26 | -------------------------------------------------------------------------------- /hacking/README.md: -------------------------------------------------------------------------------- 1 | 'Hacking' directory tools 2 | ========================= 3 | 4 | aws_config 5 | ---------- 6 | 7 | AWS policy definitions for testing AWS modules. These differ from the 8 | policies in the aws/policy directory in that they are more broad, 9 | not containing any region or account restrictions, and include policies 10 | for modules not currently supported in CI. 11 | 12 | These policies are provided as a guideline for permissions needed to 13 | develop against the modules in the `community.amazon` and `ansible.amazon` 14 | collections. 15 | 16 | Usage 17 | ----- 18 | 19 | Policies can be deployed to an AWS account using the `setup_iam.yml` 20 | playbook. 21 | 22 | ``` 23 | export ADMIN_PROFILE=$your_profile_name 24 | ansible-playbook hacking/aws_config/setup-iam.yml -e region=us-east-2 \ 25 | -e profile=$ADMIN_PROFILE -e iam_group=ansible_test -vv 26 | ``` 27 | -------------------------------------------------------------------------------- /hacking/aws_config/ci_policies: -------------------------------------------------------------------------------- 1 | ../../aws/policy -------------------------------------------------------------------------------- /hacking/aws_config/setup-iam.yml: -------------------------------------------------------------------------------- 1 | # Usage: ansible-playbook setup-iam.yml -vv 2 | # 3 | # Creates IAM policies and associates them with two iam_groups. These groups 4 | # can then be associated with an appropriate user: 5 | # 6 | # Groups: 7 | # - ansible-integration-ci 8 | # - Mirrors the permissions used by the the AWS collections CI 9 | # - ansible-integration-unsupported 10 | # - Additional Permissions on top of the 'CI' permissions 11 | # required to run the 'unsupported' tests 12 | # 13 | # Policies: 14 | # - AnsibleIntegration-CI-* 15 | # - Managed Policies used by the {{ iam_group_prefix }}-ci group 16 | # - AnsibleIntegration-Unsupported-* 17 | # - Managed Policies used by the {{ iam_group_prefix }}-unsupported group 18 | # 19 | # Supported variables: 20 | # - region: (default: us-east-1) The AWS region that any region-restricted 21 | # policies will be limited to 22 | # - iam_group_prefix: (default: ansible-integration) the prefix used when 23 | # creating IAM groups 24 | # - iam_policy_prefix: (default: AnsibleIntegration) the prefix used when 25 | # creating IAM policies 26 | # - iam_user: (default: None) An IAM user to attach to the groups 27 | # 28 | # Note: 29 | # - In general you will want any test user to be a member of **both** the -ci 30 | # and the -unsupported groups 31 | 32 | - hosts: localhost 33 | connection: local 34 | gather_facts: no 35 | vars: 36 | aws_region: "{{ region | default('us-east-1') }}" 37 | iam_group_prefix: "{{ group_prefix | default('ansible-integration') }}" 38 | iam_policy_prefix: "{{ policy_prefix | default('AnsibleIntegratation') }}" 39 | 40 | module_defaults: 41 | group/aws: 42 | profile: '{{ profile | default(omit) }}' 43 | 44 | # # Needed if you're running Ansible 2.10+ 45 | # collections: 46 | # - amazon.aws 47 | # - community.aws 48 | 49 | tasks: 50 | 51 | - name: Get aws account ID 52 | aws_caller_info: 53 | register: aws_caller_info 54 | 55 | - name: Set aws_account_id fact 56 | set_fact: 57 | aws_account_id: "{{ aws_caller_info.account }}" 58 | 59 | - name: Ensure Managed IAM policies exist (CI) 60 | vars: 61 | _policy: "{{ (item | basename | regex_replace('\\.yaml', '') | regex_replace('\\W+', ' ')).split(' ') | map('capitalize') | join('') }}" 62 | iam_managed_policy: 63 | policy_name: "{{ iam_policy_prefix }}-CI-{{ _policy }}" 64 | policy: "{{ lookup('template', item) | from_yaml | to_json }}" 65 | state: present 66 | with_fileglob: "ci_policies/*.yaml" 67 | register: iam_managed_policies_ci 68 | 69 | - name: Ensure Managed IAM policies exist (unsupported) 70 | vars: 71 | _policy: "{{ (item | basename | regex_replace('\\.yaml', '') | regex_replace('\\W+', ' ')).split(' ') | map('capitalize') | join('') }}" 72 | iam_managed_policy: 73 | policy_name: "{{ iam_policy_prefix }}-Unsupported-{{ _policy }}" 74 | policy: "{{ lookup('template', item) | from_yaml | to_json }}" 75 | state: present 76 | with_fileglob: "test_policies/*.yaml" 77 | register: iam_managed_policies_unsupported 78 | 79 | - debug: 80 | msg: "{{ iam_managed_policies_ci | json_query('results[].policy.policy_name') }}" 81 | - debug: 82 | msg: "{{ iam_managed_policies_unsupported | json_query('results[].policy.policy_name') }}" 83 | 84 | - name: Ensure IAM CI group exists and attach managed policies 85 | iam_group: 86 | name: "{{ iam_group_prefix }}-ci" 87 | state: present 88 | managed_policy: "{{ iam_managed_policies_ci | json_query('results[].policy.policy_name') }}" 89 | purge_policy: yes 90 | users: '{{ iam_user | default(omit)}}' 91 | 92 | - name: Ensure IAM Unsupported group exists and attach managed policies 93 | iam_group: 94 | name: "{{ iam_group_prefix }}-unsupported" 95 | state: present 96 | managed_policy: "{{ iam_managed_policies_unsupported | json_query('results[].policy.policy_name') }}" 97 | purge_policy: yes 98 | users: '{{ iam_user | default(omit)}}' 99 | -------------------------------------------------------------------------------- /hacking/aws_config/test-policies.yml: -------------------------------------------------------------------------------- 1 | # Usage: ansible-playbook test-policies.yml -vv 2 | # 3 | - hosts: localhost 4 | connection: local 5 | gather_facts: no 6 | vars: 7 | ansible_python_interpreter: 'env python' 8 | 9 | aws_region: "{{ region | default('us-east-1') }}" 10 | iam_group_prefix: "{{ group_prefix | default('ansible-integration') }}" 11 | iam_policy_prefix: "{{ policy_prefix | default('AnsibleIntegratation') }}" 12 | aws_account_id: "{{ test_account_id | default('123456789012') }}" 13 | ci_policies: '../../aws/policy/*.yaml' 14 | test_policies: 'test_policies/*.yaml' 15 | 16 | tasks: 17 | - name: 'Check size of policies' 18 | vars: 19 | raw_policy: "{{ lookup('template', item) | from_yaml | to_json }}" 20 | with_fileglob: 21 | - '{{ test_policies }}' 22 | - '{{ ci_policies }}' 23 | set_fact: 24 | policy: "{{ raw_policy }}" 25 | policy_size: "{{ raw_policy | length }}" 26 | # https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_iam-quotas.html#reference_iam-quotas-entity-length 27 | # Actual limit is 6144 this gives us a little buffer 28 | oversized: "{{ raw_policy | length > 6124 }}" 29 | register: policies 30 | 31 | - name: 'Fail if policy is too large' 32 | vars: 33 | _query: 'results[?ansible_facts.oversized].item' 34 | oversized_policies: "{{ policies | json_query(_query) | list }}" 35 | debug: 36 | var: oversized_policies 37 | failed_when: 38 | - oversized_policies | length > 0 39 | 40 | - name: 'Dump generated policies as files for additional testing/debugging' 41 | when: 'dump_policies | default(False) | bool' 42 | block: 43 | - name: Create location for policies 44 | tempfile: 45 | state: directory 46 | suffix: test-policies 47 | register: tmp_dir 48 | 49 | - name: Generate CI policies 50 | vars: 51 | _policy: "{{ (item | basename | regex_replace('\\.yaml', '') | regex_replace('\\W+', ' ')).split(' ') | map('capitalize') | join('') }}" 52 | copy: 53 | dest: '{{ tmp_dir.path }}/{{ iam_policy_prefix }}-CI-{{ _policy }}.json' 54 | content: "{{ lookup('template', item) | from_yaml | to_json }}" 55 | with_fileglob: 56 | - '{{ ci_policies }}' 57 | register: iam_managed_policies_ci 58 | 59 | - name: Generate unsupported policies 60 | vars: 61 | _policy: "{{ (item | basename | regex_replace('\\.yaml', '') | regex_replace('\\W+', ' ')).split(' ') | map('capitalize') | join('') }}" 62 | copy: 63 | dest: '{{ tmp_dir.path }}/{{ iam_policy_prefix }}-Unsupported-{{ _policy }}.json' 64 | content: "{{ lookup('template', item) | from_yaml | to_json }}" 65 | with_fileglob: 66 | - '{{ test_policies }}' 67 | register: iam_managed_policies_unsupported 68 | -------------------------------------------------------------------------------- /hacking/aws_config/test_policies/compute.yaml: -------------------------------------------------------------------------------- 1 | Version: '2012-10-17' 2 | Statement: 3 | - Sid: AllowGlobalUnrestrictedResourceActionsWhichIncurNoFees 4 | Effect: Allow 5 | Action: 6 | - autoscaling:DescribeAutoScalingGroups 7 | - autoscaling:DescribeInstanceRefreshes 8 | - autoscaling:DescribeLaunchConfigurations 9 | - autoscaling:DescribePolicies 10 | - ec2:Describe* 11 | - lambda:CreateEventSourceMapping 12 | - lambda:GetAccountSettings 13 | - lambda:GetEventSourceMapping 14 | - lambda:List* 15 | - lambda:TagResource 16 | - lambda:UntagResource 17 | - elasticloadbalancing:Describe* 18 | Resource: "*" 19 | 20 | - Sid: AllowGlobalUnrestrictedResourceActionsWhichIncurFees 21 | Effect: Allow 22 | Action: 23 | - ec2:ReportInstanceStatus 24 | Resource: "*" 25 | 26 | - Sid: AllowGlobalResourceRestrictedActionsWhichIncurNoFees 27 | Effect: Allow 28 | Action: 29 | - lambda:CreateAlias 30 | - lambda:DeleteAlias 31 | - lambda:GetAlias 32 | - lambda:PublishVersion 33 | - lambda:UpdateAlias 34 | - lambda:UpdateEventSourceMapping 35 | - lambda:UpdateFunctionCode 36 | Resource: 37 | - 'arn:aws:lambda:{{ aws_region }}:{{ aws_account_id }}:function:*' 38 | 39 | - Sid: AllowGlobalRestrictedResourceActionsWhichIncurFees 40 | Effect: Allow 41 | Action: 42 | - autoscaling:*LaunchConfiguration 43 | - autoscaling:*LoadBalancers 44 | - autoscaling:*AutoScalingGroup 45 | - autoscaling:*MetricsCollection 46 | - autoscaling:PutScalingPolicy 47 | - autoscaling:DeletePolicy 48 | - autoscaling:*Tags 49 | - elasticloadbalancing:*LoadBalancer 50 | - elasticloadbalancing:*LoadBalancers 51 | - elasticloadbalancing:*LoadBalancerListeners 52 | - elasticloadbalancing:*TargetGroup 53 | - elasticloadbalancing:Create* 54 | - elasticloadbalancing:Delete* 55 | - elasticloadbalancing:DeregisterInstancesFromLoadBalancer 56 | - elasticloadbalancing:Modify* 57 | - elasticloadbalancing:Register* 58 | - elasticloadbalancing:Deregister* 59 | - elasticloadbalancing:Remove* 60 | Resource: 61 | - 'arn:aws:autoscaling:{{ aws_region }}:{{ aws_account_id }}:*' 62 | 63 | - Sid: AllowRoleManagement 64 | Effect: Allow 65 | Action: 66 | - iam:PassRole 67 | Resource: 68 | - 'arn:aws:iam::{{ aws_account_id }}:role/ecsInstanceRole' 69 | - 'arn:aws:iam::{{ aws_account_id }}:role/ec2InstanceRole' 70 | - 'arn:aws:iam::{{ aws_account_id }}:role/ecsServiceRole' 71 | - 'arn:aws:iam::{{ aws_account_id }}:role/aws_eks_cluster_role' 72 | - 'arn:aws:iam::{{ aws_account_id }}:role/ecsTaskExecutionRole' 73 | -------------------------------------------------------------------------------- /hacking/aws_config/test_policies/container-policy.yaml: -------------------------------------------------------------------------------- 1 | Version: '2012-10-17' 2 | Statement: 3 | - Sid: AllowGlobalUnrestrictedResourceActionsWhichIncurNoFees 4 | Effect: Allow 5 | Action: 6 | - application-autoscaling:Describe* 7 | - application-autoscaling:PutScalingPolicy 8 | - application-autoscaling:RegisterScalableTarget 9 | - ecs:DeregisterTaskDefinition 10 | - ecs:CreateService 11 | - ecs:DeleteService 12 | - ecs:Describe* 13 | - ecs:List* 14 | - ecs:PutAccountSetting 15 | - ecs:RegisterTaskDefinition 16 | - ecs:UpdateService 17 | - elasticloadbalancing:Describe* 18 | Resource: "*" 19 | 20 | - Sid: AllowGlobalUnrestrictedResourceActionsWhichIncurFees 21 | Effect: Allow 22 | Action: 23 | - ecs:CreateCluster 24 | - ecs:DeleteCluster 25 | - ecs:RunTask 26 | - ecs:StartTask 27 | - ecs:StopTask 28 | Resource: "*" 29 | 30 | # - Sid: AllowGlobalResourceRestrictedActionsWhichIncurNoFees 31 | # Effect: Allow 32 | # Action: 33 | # - 34 | # Resource: 35 | # - 36 | 37 | # - Sid: AllowGlobalRestrictedResourceActionsWhichIncurFees 38 | # Effect: Allow 39 | # Action: 40 | # - 41 | # Resource: 42 | # - 43 | -------------------------------------------------------------------------------- /hacking/aws_config/test_policies/database-policy.yaml: -------------------------------------------------------------------------------- 1 | Version: '2012-10-17' 2 | Statement: 3 | - Action: iam:CreateServiceLinkedRole 4 | Effect: Allow 5 | Resource: 'arn:aws:iam::*:role/aws-service-role/rds.amazonaws.com/AWSServiceRoleForRDS' 6 | Condition: 7 | StringLike: 8 | iam:AWSServiceName: rds.amazonaws.com 9 | - Action: iam:CreateServiceLinkedRole 10 | Effect: Allow 11 | Resource: 'arn:aws:iam::*:role/aws-service-role/redshift.amazonaws.com/AWSServiceRoleForRedshift' 12 | Condition: 13 | StringLike: 14 | iam:AWSServiceName: redshift.amazonaws.com 15 | 16 | - Sid: AllowGlobalUnrestrictedResourceActionsWhichIncurNoFees 17 | Effect: Allow 18 | Action: 19 | - rds:ListTagsForResource 20 | - rds:DescribeDBInstances 21 | - rds:DescribeDBParameterGroups 22 | - rds:DescribeDBParameters 23 | - rds:DescribeDBSnapshots 24 | Resource: "*" 25 | 26 | - Sid: AllowGlobalUnrestrictedResourceActionsWhichIncurFees 27 | Effect: Allow 28 | Action: 29 | - dms:CreateEndpoint 30 | - dms:DeleteEndpoint 31 | - dms:DescribeEndpoints 32 | - dms:ModifyEndpoint 33 | Resource: "*" 34 | 35 | - Sid: AllowGlobalResourceRestrictedActionsWhichIncurNoFees 36 | Effect: Allow 37 | Action: 38 | - rds:RebootDBInstance 39 | - rds:AddTagsToResource 40 | Resource: 41 | - 'arn:aws:rds:{{ aws_region }}:{{ aws_account_id }}:snapshot:ansible-test*' 42 | - 'arn:aws:rds:{{ aws_region }}:{{ aws_account_id }}:db:ansible-test*' 43 | 44 | - Sid: AllowGlobalRestrictedResourceActionsWhichIncurFees 45 | Effect: Allow 46 | Action: 47 | - rds:CreateDBInstance 48 | - rds:DeleteDBInstance 49 | - rds:ModifyDBInstance 50 | - rds:PromoteReadReplica 51 | - rds:RestoreDBInstanceToPointInTime 52 | - rds:StartDBInstance 53 | - rds:StopDBInstance 54 | - rds:CreateDBSnapshot 55 | - rds:DeleteDBInstance 56 | - rds:DeleteDBSnapshot 57 | - rds:RemoveTagsFromResource 58 | - rds:RestoreDBInstanceFromDBSnapshot 59 | - rds:CreateDBInstanceReadReplica 60 | Resource: 61 | - 'arn:aws:rds:{{ aws_region }}:{{ aws_account_id }}:snapshot:ansible-test*' 62 | - 'arn:aws:rds:{{ aws_region }}:{{ aws_account_id }}:db:ansible-test*' 63 | -------------------------------------------------------------------------------- /hacking/aws_config/test_policies/devops-policy.yaml: -------------------------------------------------------------------------------- 1 | Version: '2012-10-17' 2 | Statement: 3 | # - Sid: AllowGlobalUnrestrictedResourceActionsWhichIncurNoFees 4 | # Effect: Allow 5 | # Action: 6 | # - 7 | # Resource: "*" 8 | 9 | - Sid: AllowGlobalUnrestrictedResourceActionsWhichIncurFees 10 | Effect: Allow 11 | Action: 12 | - codecommit:*Repository 13 | Resource: "*" 14 | 15 | # - Sid: AllowGlobalResourceRestrictedActionsWhichIncurNoFees 16 | # Effect: Allow 17 | # Action: 18 | # - 19 | # Resource: 20 | # - 21 | 22 | # - Sid: AllowGlobalRestrictedResourceActionsWhichIncurFees 23 | # Effect: Allow 24 | # Action: 25 | # - 26 | # Resource: 27 | # - 28 | -------------------------------------------------------------------------------- /hacking/aws_config/test_policies/networking.yaml: -------------------------------------------------------------------------------- 1 | Version: '2012-10-17' 2 | Statement: 3 | - Sid: AllowGlobalUnrestrictedResourceActionsWhichIncurNoFees 4 | Effect: Allow 5 | Action: 6 | - ec2:Describe* 7 | Resource: "*" 8 | 9 | - Sid: AllowGlobalUnrestrictedResourceActionsWhichIncurFees 10 | Effect: Allow 11 | Action: 12 | - cloudfront:CreateDistribution 13 | - cloudfront:CreateDistributionWithTags 14 | - cloudfront:CreateCloudFrontOriginAccessIdentity 15 | - cloudfront:DeleteDistribution 16 | - cloudfront:GetDistribution 17 | - cloudfront:GetStreamingDistribution 18 | - cloudfront:GetDistributionConfig 19 | - cloudfront:GetStreamingDistributionConfig 20 | - cloudfront:GetInvalidation 21 | - cloudfront:ListDistributions 22 | - cloudfront:ListDistributionsByWebACLId 23 | - cloudfront:ListInvalidations 24 | - cloudfront:ListStreamingDistributions 25 | - cloudfront:ListTagsForResource 26 | - cloudfront:TagResource 27 | - cloudfront:UntagResource 28 | - cloudfront:UpdateDistribution 29 | Resource: '*' 30 | 31 | # - Sid: AllowGlobalResourceRestrictedActionsWhichIncurNoFees 32 | # Effect: Allow 33 | # Action: 34 | # - 35 | # Resource: 36 | # - 37 | 38 | # - Sid: AllowGlobalRestrictedResourceActionsWhichIncurFees 39 | # Effect: Allow 40 | # Action: 41 | # - 42 | # Resource: 43 | # - 44 | -------------------------------------------------------------------------------- /hacking/aws_config/test_policies/security-services.yaml: -------------------------------------------------------------------------------- 1 | Version: '2012-10-17' 2 | Statement: 3 | - Sid: AllowGlobalUnrestrictedResourceActionsWhichIncurNoFees 4 | Effect: Allow 5 | Action: 6 | - cloudtrail:DescribeTrails 7 | - cloudtrail:ListTags 8 | - cloudtrail:ListPublicKeys 9 | - iam:GetPolicy 10 | - iam:GetPolicyVersion 11 | - iam:GetRolePolicy 12 | - iam:ListAttachedGroupPolicies 13 | - iam:ListAttachedRolePolicies 14 | - iam:ListAttachedUserPolicies 15 | - iam:ListInstanceProfilesForRole 16 | - iam:ListGroups 17 | - iam:ListUsers 18 | - iam:ListPolicies 19 | - iam:GetAccountPasswordPolicy 20 | - iam:UpdateAccountPasswordPolicy 21 | - iam:DeleteAccountPasswordPolicy 22 | Resource: "*" 23 | 24 | - Sid: AllowGlobalUnrestrictedResourceActionsWhichIncurFees 25 | Effect: Allow 26 | Action: 27 | # Legacied because the current (minimal) tests don't use 28 | # { resource_prefix } yet 29 | - iam:DeleteServerCertificate 30 | - iam:GetServerCertificate 31 | - iam:ListServerCertificates 32 | - iam:UpdateServerCertificate 33 | - iam:UploadServerCertificate 34 | - waf:DeleteLoggingConfiguration 35 | - waf:DeletePermissionPolicy 36 | - waf:GetLoggingConfiguration 37 | - waf:GetPermissionPolicy 38 | - waf:GetSampledRequests 39 | - waf:ListLoggingConfigurations 40 | - waf:PutLoggingConfiguration 41 | - waf:PutPermissionPolicy 42 | Resource: "*" 43 | 44 | - Sid: AllowGlobalResourceRestrictedActionsWhichIncurNoFees 45 | Effect: Allow 46 | Action: 47 | - iam:AddUserToGroup 48 | - iam:AttachUserPolicy 49 | - iam:AttachRolePolicy 50 | - iam:CreateAccessKey 51 | - iam:CreateGroup 52 | - iam:CreatePolicy 53 | - iam:CreateUser 54 | - iam:DeleteAccessKey 55 | - iam:DeleteGroup 56 | - iam:DeleteLoginProfile 57 | - iam:DeletePolicy 58 | - iam:DeleteRolePermissionsBoundary 59 | - iam:DeleteRolePolicy 60 | - iam:DeleteUser 61 | - iam:DetachUserPolicy 62 | - iam:DetachRolePolicy 63 | - iam:GetGroup 64 | - iam:GetUser 65 | - iam:ListAccessKeys 66 | - iam:ListAttachedGroupPolicies 67 | - iam:ListAttachedUserPolicies 68 | - iam:ListEntitiesForPolicy 69 | - iam:ListGroupsForUser 70 | - iam:ListMFADevices 71 | - iam:ListPolicyVersions 72 | - iam:ListServiceSpecificCredentials 73 | - iam:ListSigningCertificates 74 | - iam:ListSSHPublicKeys 75 | - iam:ListUserPolicies 76 | - iam:PassRole 77 | - iam:PutRolePermissionsBoundary 78 | - iam:PutRolePolicy 79 | - iam:RemoveUserFromGroup 80 | - iam:TagUser 81 | - iam:UntagUser 82 | - iam:UpdateAccessKey 83 | - iam:UpdateAssumeRolePolicy 84 | - iam:UpdateGroup 85 | - iam:UpdateRole 86 | - iam:UpdateRoleDescription 87 | - iam:UpdateUser 88 | - logs:DescribeLogGroups 89 | Resource: 90 | - 'arn:aws:iam::{{ aws_account_id }}:policy/ansible-test-*' 91 | - 'arn:aws:iam::{{ aws_account_id }}:role/ansible-test-*' 92 | - 'arn:aws:iam::{{ aws_account_id }}:user/ansible-test*' 93 | - 'arn:aws:iam::{{ aws_account_id }}:group/ansible-test*' 94 | - 'arn:aws:logs:{{ aws_region }}:{{ aws_account_id }}:log-group:*' 95 | 96 | - Sid: AllowGlobalRestrictedResourceActionsWhichIncurFees 97 | Effect: Allow 98 | Action: 99 | - logs:CreateLogGroup 100 | - logs:PutRetentionPolicy 101 | - logs:DeleteLogGroup 102 | # Legacied 103 | - cloudtrail:* 104 | - secretsmanager:* 105 | Resource: 106 | - 'arn:aws:cloudtrail:{{ aws_region }}:{{ aws_account_id }}:trail/ansible-test-*' 107 | - 'arn:aws:logs:{{ aws_region }}:{{ aws_account_id }}:log-group:ansible-test*' 108 | - 'arn:aws:secretsmanager:{{ aws_region }}:{{ aws_account_id }}:secret:ansible-test*' 109 | 110 | - Sid: AllowReplacementOfSpecificInstanceProfiles 111 | Effect: Allow 112 | Action: 113 | - ec2:ReplaceIamInstanceProfileAssociation 114 | Resource: '*' 115 | Condition: 116 | ArnEquals: 117 | ec2:InstanceProfile: 'arn:aws:iam::{{ aws_account_id }}:instance-profile/ansible-test-*' 118 | -------------------------------------------------------------------------------- /hacking/aws_config/test_policies/storage.yaml: -------------------------------------------------------------------------------- 1 | Version: '2012-10-17' 2 | Statement: 3 | # - Sid: AllowGlobalUnrestrictedResourceActionsWhichIncurNoFees 4 | # Effect: Allow 5 | # Action: 6 | # - 7 | # Resource: "*" 8 | 9 | - Sid: AllowGlobalUnrestrictedResourceActionsWhichIncurFees 10 | Effect: Allow 11 | Action: 12 | - elasticfilesystem:* 13 | Resource: "*" 14 | 15 | - Sid: AllowGlobalResourceRestrictedActionsWhichIncurNoFees 16 | Effect: Allow 17 | Action: 18 | - s3:ListJobs 19 | - s3:DeleteAccessPoint 20 | - s3:DeleteAccessPointPolicy 21 | - s3:DeleteBucketWebsite 22 | - s3:ListAccessPoints 23 | - s3:ListMultipartUploadParts 24 | Resource: 25 | - 'arn:aws:s3:::ansible-test-*' 26 | - 'arn:aws:s3:::ansible-test-*/*' 27 | 28 | # - Sid: AllowGlobalRestrictedResourceActionsWhichIncurFees 29 | # Effect: Allow 30 | # Action: 31 | # - 32 | # Resource: 33 | # - 'arn:aws:s3:::ansible-test-*' 34 | # - 'arn:aws:s3:::ansible-test-*/*' 35 | -------------------------------------------------------------------------------- /pycodestyle.ini: -------------------------------------------------------------------------------- 1 | [pycodestyle] 2 | max-line-length = 160 3 | -------------------------------------------------------------------------------- /pylint.rc: -------------------------------------------------------------------------------- 1 | [FORMAT] 2 | max-line-length=160 3 | reports=no 4 | score=no 5 | disable= 6 | invalid-name, 7 | missing-docstring, 8 | protected-access, 9 | similarities, 10 | too-few-public-methods, 11 | too-many-arguments, 12 | too-many-branches, 13 | too-many-locals, 14 | too-many-statements, 15 | unused-argument, 16 | -------------------------------------------------------------------------------- /requirements.yml: -------------------------------------------------------------------------------- 1 | collections: 2 | - name: mattclay.aws 3 | version: '2.0.0' 4 | - name: community.aws 5 | version: '3.3.0' 6 | - name: community.general 7 | version: '>=6.0.0' 8 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skipsdist=True 3 | envlist=pycodestyle,pylint,yamllint,policy 4 | 5 | [test-deps] 6 | deps = 7 | -cconstraints.txt 8 | -raws/requirements.txt 9 | 10 | [testenv:pylint] 11 | description = Check with Pylint 12 | deps = 13 | # PyYAML is only used by clean.py, we don't want to add it in 14 | # aws/requirements.txt or it will end up in the Lambda package. 15 | PyYAML 16 | {[test-deps]deps} 17 | pylint 18 | commands = pylint --rcfile {toxinidir}/pylint.rc {toxinidir}/aws 19 | 20 | [testenv:pycodestyle] 21 | description = Check with pycodestyle 22 | deps = 23 | {[test-deps]deps} 24 | pycodestyle 25 | commands = pycodestyle --config {toxinidir}/pycodestyle.ini {toxinidir}/aws 26 | 27 | [testenv:yamllint] 28 | description = Check with YAMLlint 29 | deps = 30 | {[test-deps]deps} 31 | yamllint 32 | commands = yamllint --config-file {toxinidir}/.yamllint {toxinidir} 33 | 34 | [testenv:policy] 35 | description = Run the test-policies playbook 36 | deps = 37 | {[test-deps]deps} 38 | ansible-core 39 | commands = 40 | ansible-galaxy collection install -r {toxinidir}/requirements.yml 41 | ansible-playbook -i localhost {toxinidir}/hacking/aws_config/test-policies.yml 42 | setenv = 43 | ANSIBLE_COLLECTIONS_PATHS={toxworkdir}/ansible 44 | --------------------------------------------------------------------------------