From 0be6ff2f32195ee6c39146b84faabf446170ad2a Mon Sep 17 00:00:00 2001 From: Dennis Carey Date: Fri, 11 Aug 2023 10:41:58 -0400 Subject: [PATCH 01/10] Create lambda and terraform --- CHANGELOG.md | 28 +- Dockerfile | 7 +- README.md | 124 ++++++-- main.tf | 122 +++++++ src/delete_default_cloudtrail.py | 299 ++++++++++++++++++ src/requirements.txt | 2 + .../cloudtrail/main.tf | 66 ++++ .../cloudtrail/outputs.tf | 7 + .../cloudtrail/variables.tf | 15 + tests/test_delete_default_cloudtrail/main.tf | 100 ++++++ variables.tf | 85 +++++ versions.tf | 10 + 12 files changed, 817 insertions(+), 48 deletions(-) create mode 100644 main.tf create mode 100644 src/delete_default_cloudtrail.py create mode 100644 src/requirements.txt create mode 100644 tests/test_delete_default_cloudtrail/cloudtrail/main.tf create mode 100644 tests/test_delete_default_cloudtrail/cloudtrail/outputs.tf create mode 100644 tests/test_delete_default_cloudtrail/cloudtrail/variables.tf create mode 100644 tests/test_delete_default_cloudtrail/main.tf create mode 100644 variables.tf create mode 100644 versions.tf diff --git a/CHANGELOG.md b/CHANGELOG.md index 222e408..de43067 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,33 +1,13 @@ -## repo-template +## terraform-aws-tardigrade-org-new-account-delete-cloudtrail All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -### [1.2.0] (https://github.com/plus3it/repo-template/releases/tag/1.2.0) +### [1.0.0](https://github.com/plus3it/terraform-aws-tardigrade-org-new-account-delete-cloudtrail/releases/tag/1.0.0) -**Summary**: - -* Updated SHA value for Github Actions Workflows -* Updated CHANGELOG.template.md file -* Added Master branch in release workflow logic to make migration to Github Actions more efficient - -### 1.1.0 - -**Commit Delta**: N/A - -**Released**: 2023.01.27 - -**Summary**: - -* Updated workflow files to be consumable and reusable, and now points to actions-workflows repo - -### 1.0.0 - -**Commit Delta**: N/A - -**Released**: 2023.01.10 +**Released**: 2023.08.11 **Summary**: -* Initial release of capability +* Initial Release diff --git a/Dockerfile b/Dockerfile index 125a5f6..f59f87a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1 +1,6 @@ -FROM plus3it/tardigrade-ci:0.24.7 +FROM plus3it/tardigrade-ci:0.24.9 + +COPY ./src/requirements.txt /app/requirements/lambda.txt + +RUN python -m pip install --no-cache-dir \ + -r /app/requirements/lambda.txt diff --git a/README.md b/README.md index ceff70c..5486d54 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,101 @@ -# repo-template -Generic repo template for Plus3IT repositories - -To use this template: - -1. Select the green "Use this template" button, or [click here](https://github.com/plus3it/repo-template/generate). -2. Select the repo Owner, give the repo a name, enter a description, select Public or Private, and click "Create repository from template". -3. Clone the repository and create a new branch. -4. Edit the following files to customize them for the new repository: - * `LICENSE` - * Near the end of the file, edit the date and change the repository name - * `CHANGELOG.template.md` - * Rename to `CHANGELOG.md`, replacing the repo-template changelog - * Edit templated items for the new repo - * `.bumpversion.cfg` - * Edit the version number for the new repo, ask team if not sure what to - start with - * `README.md` - * Replace contents for the new repo - * `.github/` - * Inspect dependabot and workflow files in case changes are needed for - the new repo -5. Commit the changes and open a pull request +# terraform-aws-tardigrade-org-new-account-delete-cloudtrail + +A Terraform module to delete the default cloudtrail when new AWS accounts +are added or invited to an AWS Organization. + +The Lambda function is triggered for the account by an Event Rule that matches +the CreateAccountResult or InviteAccountToOrganization events. The function then +deletes the default cloudtrail and s3 objects and buckets for that account. + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3 | +| [aws](#requirement\_aws) | >= 4.9 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.9 | + +## Resources + +| Name | Type | +|------|------| +| [aws_iam_policy_document.lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [project\_name](#input\_project\_name) | Project name to prefix resources with | `string` | n/a | yes | +| [assume\_role\_name](#input\_assume\_role\_name) | Name of the IAM role that the lambda will assume in the target account | `string` | `"OrganizationAccountAccessRole"` | no | +| [cloudtrail\_name\_prefix](#input\_cloudtrail\_name\_prefix) | Name Prefix of the cloudtrail name to delete | `string` | `"cloudtrail-"` | no | +| [dry\_run](#input\_dry\_run) | Boolean toggle to control the dry-run mode of the lambda function | `bool` | `true` | no | +| [error\_not\_found](#input\_error\_not\_found) | Error if the cloudtrail name/pattern is not found | `bool` | `true` | no | +| [event\_bus\_name](#input\_event\_bus\_name) | Event bus name to create event rules in | `string` | `"default"` | no | +| [event\_types](#input\_event\_types) | Event types that will trigger this lambda | `set(string)` |
[
"CreateAccountResult",
"InviteAccountToOrganization"
]
| no | +| [lambda](#input\_lambda) | Object of optional attributes passed on to the lambda module |
object({
artifacts_dir = optional(string, "builds")
build_in_docker = optional(bool, false)
create_package = optional(bool, true)
ephemeral_storage_size = optional(number)
ignore_source_code_hash = optional(bool, true)
local_existing_package = optional(string)
memory_size = optional(number, 128)
recreate_missing_package = optional(bool, false)
runtime = optional(string, "python3.8")
s3_bucket = optional(string)
s3_existing_package = optional(map(string))
s3_prefix = optional(string)
store_on_s3 = optional(bool, false)
timeout = optional(number, 300)
})
| `{}` | no | +| [log\_level](#input\_log\_level) | Log level for lambda | `string` | `"INFO"` | no | +| [tags](#input\_tags) | Tags for resource | `map(string)` | `{}` | no | + +## Outputs + +No outputs. + + + +## CLI Option + +Steps to run via the CLI + +1. Install and configure aws cli. +2. Set AWS_PROFILE and AWS_DEFAULT_REGION (account and region that can assume the role and run commands from) +3. Review the options for the script and run + +### Script Options + +```bash +Supported Environment Variables: + 'LOG_LEVEL': defaults to 'info' + - set the desired log level ('error', 'warning', 'info' or 'debug') + + 'DRY_RUN': defaults to 'true' + - set whether actions should be simulated or live + - value of 'true' (case insensitive) will be simulated. + + 'CLOUDTRAIL_NAME_PREFIX': defaults to 'cloudtrail-' + -sets the name of the cloudtrail to delete. + +options: + -h, --help show this help message and exit + +required arguments: + --target-account-id TARGET_ACCOUNT_ID + Account number to delete default VPC resources in + + --assume-role-arn ASSUME_ROLE_ARN + ARN of IAM role to assume in the target account (case sensitive) + OR + --assume-role-name ASSUME_ROLE_NAME + Name of IAM role to assume in the target account (case sensitive) + +usage: delete_default_cloudtrail.py [-h] --target-account-id TARGET_ACCOUNT_ID (--assume-role-arn ASSUME_ROLE_ARN | --assume-role-name ASSUME_ROLE_NAME) +``` + +### Sample steps to execute in venv + +```bash +mkdir vpc_env +python3 -m venv vpc_env +source vpc_env/bin/activate +python3 -m pip install -U pip +pip3 install -r src/requirements.txt +python3 src/delete_default_cloudtrail.py --target-account-id= (--assume-role-arn= | --assume-role-name=) +deactivate +rm -rf vpc_env +``` diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..e8b5e4b --- /dev/null +++ b/main.tf @@ -0,0 +1,122 @@ +############################## +# Lambda +############################## +module "lambda" { + source = "git::https://github.com/terraform-aws-modules/terraform-aws-lambda.git?ref=v5.3.0" + + function_name = "${var.project_name}-delete-default-cloudtrail" + + description = "Lambda function deleting cloudtrail and associated bucket" + handler = "delete_default_cloudtrail.lambda_handler" + tags = var.tags + + attach_policy_json = true + policy_json = data.aws_iam_policy_document.lambda.json + + artifacts_dir = var.lambda.artifacts_dir + build_in_docker = var.lambda.build_in_docker + create_package = var.lambda.create_package + ignore_source_code_hash = var.lambda.ignore_source_code_hash + local_existing_package = var.lambda.local_existing_package + recreate_missing_package = var.lambda.recreate_missing_package + ephemeral_storage_size = var.lambda.ephemeral_storage_size + runtime = var.lambda.runtime + s3_bucket = var.lambda.s3_bucket + s3_existing_package = var.lambda.s3_existing_package + s3_prefix = var.lambda.s3_prefix + store_on_s3 = var.lambda.store_on_s3 + timeout = var.lambda.timeout + + environment_variables = { + LOG_LEVEL = var.log_level + ASSUME_ROLE_NAME = var.assume_role_name + CLOUDTRAIL_NAME_PREFIX = var.cloudtrail_name_prefix + DRY_RUN = var.dry_run + ERROR_NOT_FOUND = var.error_not_found + } + + source_path = [ + { + path = "${path.module}/src" + pip_requirements = true + patterns = ["!\\.terragrunt-source-manifest"] + } + ] + +} + +data "aws_iam_policy_document" "lambda" { + statement { + sid = "AllowAssumeRole" + + actions = [ + "sts:AssumeRole" + ] + + resources = [ + "arn:${data.aws_partition.current.partition}:iam::*:role/${var.assume_role_name}" + ] + } +} + +############################## +# Events +############################## +locals { + lambda_name = module.lambda.lambda_function_name + + event_types = { + CreateAccountResult = jsonencode( + { + "detail" : { + "eventSource" : ["organizations.amazonaws.com"], + "eventName" : ["CreateAccountResult"] + "serviceEventDetails" : { + "createAccountStatus" : { + "state" : ["SUCCEEDED"] + } + } + } + } + ) + InviteAccountToOrganization = jsonencode( + { + "detail" : { + "eventSource" : ["organizations.amazonaws.com"], + "eventName" : ["InviteAccountToOrganization"] + } + } + ) + } +} + +resource "aws_cloudwatch_event_rule" "this" { + for_each = var.event_types + + name = "${var.project_name}-${each.value}" + description = "Managed by Terraform" + event_pattern = local.event_types[each.value] + event_bus_name = var.event_bus_name + tags = var.tags +} + +resource "aws_cloudwatch_event_target" "this" { + for_each = aws_cloudwatch_event_rule.this + + rule = each.value.name + arn = module.lambda.lambda_function_arn +} + +resource "aws_lambda_permission" "events" { + for_each = aws_cloudwatch_event_rule.this + + action = "lambda:InvokeFunction" + function_name = module.lambda.lambda_function_name + principal = "events.amazonaws.com" + source_arn = each.value.arn +} + +############################## +# Common +############################## +data "aws_partition" "current" {} diff --git a/src/delete_default_cloudtrail.py b/src/delete_default_cloudtrail.py new file mode 100644 index 0000000..775422c --- /dev/null +++ b/src/delete_default_cloudtrail.py @@ -0,0 +1,299 @@ +"""Delete Cloudtrail. + +Purpose: + Delete the cloudtrail with preifx CLOUDTRAIL_NAME_PREFIX environment variable +Environment Variables: + LOG_LEVEL: (optional): sets the level for function logging + valid input: critical, error, warning, info (default), debug + CLOUDTRAIL_NAME_PREFIX: cloudtrail name to delete (default: cloudtrail-) + DRY_RUN: (optional): true or false, defaults to true + ASSUME_ROLE_NAME: Name of role sto assume +""" +from argparse import ArgumentParser, RawDescriptionHelpFormatter +import collections +import logging +import os +import sys + +import boto3 +from aws_assume_role_lib import assume_role, generate_lambda_session_name +from botocore.exceptions import ClientError + +# Standard logging config +DEFAULT_LOG_LEVEL = logging.INFO +LOG_LEVELS = collections.defaultdict( + lambda: DEFAULT_LOG_LEVEL, + { + "CRITICAL": logging.CRITICAL, + "ERROR": logging.ERROR, + "WARNING": logging.WARNING, + "INFO": logging.INFO, + "DEBUG": logging.DEBUG, + }, +) +CLOUDTRAIL_NAME_PREFIX = os.getenv("CLOUDTRAIL_NAME_PREFIX", "cloudtrail-") +ERROR_NOT_FOUND = bool(os.getenv("ERROR_NOT_FOUND", "true").lower() == "true") +DRY_RUN = os.environ.get("DRY_RUN", "true").lower() == "true" +ASSUME_ROLE_NAME = os.environ.get("ASSUME_ROLE_NAME", "OrganizationAccountAccessRole") + +# Lambda initializes a root logger that needs to be removed in order to set a +# different logging config +root = logging.getLogger() +if root.handlers: + for handler in root.handlers: + root.removeHandler(handler) + +logging.basicConfig( + format="%(asctime)s.%(msecs)03dZ [%(name)s][%(levelname)-5s]: %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S", + level=LOG_LEVELS[os.environ.get("LOG_LEVEL", "").lower()], +) + +log = logging.getLogger(__name__) + +# Get the Lambda session and clients +SESSION = boto3.Session() + + +class NoCloudtrailsFoundError(Exception): + """Error raised when there are no cloudtrails matching the name/pattern.""" + + +class MultipleCloudtrailsFoundError(Exception): + """Error raised when there are mutliple cloudtrails matching the name/pattern.""" + + +class DeleteDefaultCloudtrailError(Exception): + """All errors raised by DeleteDefaultCloudtrail Lambda.""" + + +def lambda_handler(event, context): # pylint: disable=unused-argument + """Delete the default cloudtrail and s3 bucket.""" + log.debug("AWS Event:%s", event) + + account_id = get_account_id(event) + + assume_role_arn = f"arn:{get_partition()}:iam::{account_id}:role/{ASSUME_ROLE_NAME}" + + delete_cloudtrail_resources(assume_role_arn, account_id) + + +def delete_cloudtrail_resources(assume_role_arn, account_id): + """Delete cloudtrail resources from either a lambda or main method.""" + cloudtrail_client, s3_client = get_boto3_clients(assume_role_arn, account_id) + + cloudtrail = get_cloudtrail(cloudtrail_client, CLOUDTRAIL_NAME_PREFIX) + + if cloudtrail: + if not DRY_RUN: + delete_cloudtrail(cloudtrail["Trail"]["TrailARN"], cloudtrail_client) + delete_s3_bucket(cloudtrail["Trail"]["S3BucketName"], s3_client) + else: + log.warning( + "NOT ARMED: Cloudtrail ARN: %s, S3 Bucket Name: %s", + cloudtrail["Trail"]["TrailARN"], + cloudtrail["Trail"]["S3BucketName"], + ) + else: + log.warning("Cloudtrail %s not found.", CLOUDTRAIL_NAME_PREFIX) + + +def get_cloudtrail(client, prefix): + """Find the cloudtrail by prefix.""" + # Get the cloudtrail by prefix + matching_trails = [] + try: + cloudtrails = client.describe_trails(trailNameList=[]) + for trail in cloudtrails["trailList"]: + if trail["Name"].startswith(prefix): + matching_trails.append(trail["Name"]) + + if len(matching_trails) > 1: + multiple_found_error = ( + f"Multiple ({len(matching_trails)}) " + f"cloudtrails found: {prefix}, {matching_trails}" + ) + log.error(multiple_found_error) + raise MultipleCloudtrailsFoundError(multiple_found_error) + + if len(matching_trails) == 0: + none_found_error = f"No cloudtrail found for prefix {prefix}" + if ERROR_NOT_FOUND: + log.error(none_found_error) + raise NoCloudtrailsFoundError(none_found_error) + log.warning(none_found_error) + return None + + return client.get_trail(Name=matching_trails[0]) + + except ClientError as err: + log.error("Error getting cloudtrail %s", err) + raise DeleteDefaultCloudtrailError( + f"Error getting cloudtrail by prefix {prefix}" + ) from err + + +def delete_cloudtrail(cloudtrail_arn, client): + """Stop and Delete the cloudtrail for the arn provided.""" + # Stop logging to the trail + client.stop_logging(Name=cloudtrail_arn) + # Delete the trail + client.delete_trail(Name=cloudtrail_arn) + log.debug("Cloudtrail %s has been deleted.", cloudtrail_arn) + + +def delete_s3_bucket(bucket_name, client): + """Delete the s3 bucket by name.""" + # Delete all s3 objects first + delete_s3_objects(bucket_name, client) + # Delete the s3 bucket + client.delete_bucket(Bucket=bucket_name) + log.debug("S3 bucket %s has been deleted.", bucket_name) + + +def get_boto3_clients(assume_role_arn, account_id): + """Get the cloudtrail and s3 clients.""" + # Assume the session + assumed_role_session = get_assumed_role_session(account_id, assume_role_arn) + # Create the cloudtrail and s3 clients + cloudtrail_client = assumed_role_session.client("cloudtrail") + s3_client = assumed_role_session.client("s3") + return cloudtrail_client, s3_client + + +def delete_s3_objects(bucket_name, client): + """Delete all objects from the s3 bucket.""" + # Get all objects from the s3 bucket + objects = client.list_objects_v2(Bucket=bucket_name)["Contents"] + # Delete all objects from the s3 bucket + for obj in objects: + client.delete_object(Bucket=bucket_name, Key=obj["Key"]) + + log.debug("All objects from s3 bucket %s have been deleted.", bucket_name) + + +def get_new_account_id(event): + """Return account id for new account events.""" + return event["detail"]["serviceEventDetails"]["createAccountStatus"]["accountId"] + + +def get_invite_account_id(event): + """Return account id for invite account events.""" + return event["detail"]["requestParameters"]["target"]["id"] + + +def get_account_id(event): + """Return account id for supported events.""" + event_name = event["detail"]["eventName"] + get_account_id_strategy = { + "CreateAccountResult": get_new_account_id, + "InviteAccountToOrganization": get_invite_account_id, + } + return get_account_id_strategy[event_name](event) + + +def get_assumed_role_session(account_id, role_arn): + """Get boto3 session.""" + function_name = os.environ.get( + "AWS_LAMBDA_FUNCTION_NAME", os.path.basename(__file__) + ) + + role_session_name = generate_lambda_session_name(function_name) + + # Assume the session + assumed_role_session = assume_role( + SESSION, role_arn, RoleSessionName=role_session_name, validate=False + ) + # do stuff with the assumed role using assumed_role_session + log.debug( + "Assumed identity for account %s is %s", + account_id, + assumed_role_session.client("sts").get_caller_identity()["Arn"], + ) + return assumed_role_session + + +def get_partition(): + """Return AWS partition.""" + sts = boto3.client("sts") + return sts.get_caller_identity()["Arn"].split(":")[1] + + +def cli_main(target_account_id, assume_role_arn=None, assume_role_name=None): + """Process cli assume_role_name arg and pass to main.""" + log.debug( + "CLI - target_account_id=%s assume_role_arn=%s assume_role_name=%s", + target_account_id, + assume_role_arn, + assume_role_name, + ) + + if assume_role_name: + assume_role_arn = ( + f"arn:{get_partition()}:iam::{target_account_id}:role/{assume_role_name}" + ) + log.info("assume_role_arn for provided role name is '%s'", assume_role_arn) + + main(target_account_id, assume_role_arn) + + +def main(target_account_id, assume_role_arn): + """Assume role and delete cloudtrail resources.""" + log.debug( + "Main identity is %s", + SESSION.client("sts").get_caller_identity()["Arn"], + ) + + delete_cloudtrail_resources( + assume_role_arn, + target_account_id, + ) + + if DRY_RUN: + log.debug("Dry Run listed all resources that would be deleted") + else: + log.debug("Deleted cloudtrail associated s3 bucket/objects") + + +if __name__ == "__main__": + + def create_args(): + """Return parsed arguments.""" + parser = ArgumentParser( + formatter_class=RawDescriptionHelpFormatter, + description=""" +Delete Default Cloudtrail for provided target account. + +Supported Environment Variables: + 'LOG_LEVEL': defaults to 'info' + - set the desired log level ('error', 'warning', 'info' or 'debug') + + 'DRY_RUN': defaults to 'true' + - set whether actions should be simulated or live + - value of 'true' (case insensitive) will be simulated. + + CLOUDTRAIL_NAME_PREFIX: cloudtrail name prefix to delete (default: cloudtrail-) +""", + ) + required_args = parser.add_argument_group("required named arguments") + required_args.add_argument( + "--target-account-id", + required=True, + type=str, + help="Account number to delete default cloudtrail resources in", + ) + mut_x_group = parser.add_mutually_exclusive_group(required=True) + mut_x_group.add_argument( + "--assume-role-arn", + type=str, + help="ARN of IAM role to assume in the target account (case sensitive)", + ) + mut_x_group.add_argument( + "--assume-role-name", + type=str, + help="Name of IAM role to assume in the target account (case sensitive)", + ) + + return parser.parse_args() + + sys.exit(cli_main(**vars(create_args()))) diff --git a/src/requirements.txt b/src/requirements.txt new file mode 100644 index 0000000..d6ccd2a --- /dev/null +++ b/src/requirements.txt @@ -0,0 +1,2 @@ +aws-assume-role-lib==2.10.0 +boto3==1.28.24 diff --git a/tests/test_delete_default_cloudtrail/cloudtrail/main.tf b/tests/test_delete_default_cloudtrail/cloudtrail/main.tf new file mode 100644 index 0000000..02b5d8b --- /dev/null +++ b/tests/test_delete_default_cloudtrail/cloudtrail/main.tf @@ -0,0 +1,66 @@ +data "aws_caller_identity" "current" {} +data "aws_partition" "current" {} +data "aws_region" "current" {} + +resource "aws_cloudtrail" "this" { + name = var.cloudtrail_name + s3_bucket_name = aws_s3_bucket.this.id + s3_key_prefix = "prefix" + include_global_service_events = false + tags = var.tags +} + +resource "aws_s3_bucket" "this" { + bucket = var.cloudtrail_s3_name + force_destroy = true + tags = var.tags +} + +data "aws_iam_policy_document" "this" { + statement { + sid = "AWSCloudTrailAclCheck" + effect = "Allow" + + principals { + type = "Service" + identifiers = ["cloudtrail.amazonaws.com"] + } + + actions = ["s3:GetBucketAcl"] + resources = [aws_s3_bucket.this.arn] + condition { + test = "StringEquals" + variable = "aws:SourceArn" + values = ["arn:${data.aws_partition.current.partition}:cloudtrail:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:trail/${var.cloudtrail_name}"] + } + } + + statement { + sid = "AWSCloudTrailWrite" + effect = "Allow" + + principals { + type = "Service" + identifiers = ["cloudtrail.amazonaws.com"] + } + + actions = ["s3:PutObject"] + resources = ["${aws_s3_bucket.this.arn}/prefix/AWSLogs/${data.aws_caller_identity.current.account_id}/*"] + + condition { + test = "StringEquals" + variable = "s3:x-amz-acl" + values = ["bucket-owner-full-control"] + } + condition { + test = "StringEquals" + variable = "aws:SourceArn" + values = ["arn:${data.aws_partition.current.partition}:cloudtrail:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:trail/${var.cloudtrail_name}"] + } + } +} + +resource "aws_s3_bucket_policy" "this" { + bucket = aws_s3_bucket.this.id + policy = data.aws_iam_policy_document.this.json +} diff --git a/tests/test_delete_default_cloudtrail/cloudtrail/outputs.tf b/tests/test_delete_default_cloudtrail/cloudtrail/outputs.tf new file mode 100644 index 0000000..d57bf16 --- /dev/null +++ b/tests/test_delete_default_cloudtrail/cloudtrail/outputs.tf @@ -0,0 +1,7 @@ +output "s3_bucket_arn" { + value = aws_s3_bucket.this.arn +} + +output "cloudtrail_arn" { + value = aws_cloudtrail.this.arn +} diff --git a/tests/test_delete_default_cloudtrail/cloudtrail/variables.tf b/tests/test_delete_default_cloudtrail/cloudtrail/variables.tf new file mode 100644 index 0000000..d3203e1 --- /dev/null +++ b/tests/test_delete_default_cloudtrail/cloudtrail/variables.tf @@ -0,0 +1,15 @@ +variable "cloudtrail_name" { + description = "Name of the test cloudtrail to create" + type = string +} + +variable "cloudtrail_s3_name" { + description = "Name of the s3 bucket for the test cloudtrail" + type = string +} + +variable "tags" { + description = "Tags for resource" + type = map(string) + default = {} +} diff --git a/tests/test_delete_default_cloudtrail/main.tf b/tests/test_delete_default_cloudtrail/main.tf new file mode 100644 index 0000000..6e07c23 --- /dev/null +++ b/tests/test_delete_default_cloudtrail/main.tf @@ -0,0 +1,100 @@ +locals { + cloudtrail_name_prefix = "cloudtrail-${local.id}-" + cloudtrail_suffix = "test-deletion" + + id = random_string.id.result + project = "test-delete-ct-${local.id}" + + tags = { + "project" = local.project + } +} + +module "test_cloudtrail" { + source = "./cloudtrail" + cloudtrail_name = "${local.cloudtrail_name_prefix}${local.cloudtrail_suffix}" + cloudtrail_s3_name = "${local.cloudtrail_name_prefix}${local.cloudtrail_suffix}-bucket" + tags = local.tags +} + +module "delete_default_cloudtrail" { + source = "../../" + project_name = local.project + assume_role_name = aws_iam_role.assume_role.name + cloudtrail_name_prefix = local.cloudtrail_name_prefix + dry_run = true + error_not_found = true + log_level = "DEBUG" + tags = local.tags +} + + +data "aws_caller_identity" "current" {} +data "aws_partition" "current" {} + +data "aws_iam_policy_document" "iam_cloudtrail" { + statement { + actions = [ + "cloudtrail:DescribeTrails", + "cloudtrail:GetTrail", + ] + + resources = [ + "*" + ] + } + + statement { + actions = [ + "cloudtrail:DeleteTrail", + "cloudtrail:StopLogging" + ] + + resources = [ + module.test_cloudtrail.cloudtrail_arn + ] + } + statement { + actions = [ + "s3:ListBucket", + "s3:GetObject", + "s3:DeleteObject", + "s3:DeleteBucket" + ] + + resources = [ + module.test_cloudtrail.s3_bucket_arn, + "${module.test_cloudtrail.s3_bucket_arn}/*" + ] + } +} + +resource "aws_iam_role" "assume_role" { + name = "${local.project}-delete-default-cloudtrail-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + "Sid" : "AssumeRoleCrossAccount", + "Effect" : "Allow", + "Principal" : { + "AWS" : "arn:${data.aws_partition.current.partition}:iam::${data.aws_caller_identity.current.account_id}:root" + }, + "Action" : "sts:AssumeRole" + } + ] + }) + + inline_policy { + name = local.project + policy = data.aws_iam_policy_document.iam_cloudtrail.json + } +} + +resource "random_string" "id" { + length = 6 + upper = false + special = false + numeric = false +} diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..ddbe2c9 --- /dev/null +++ b/variables.tf @@ -0,0 +1,85 @@ +variable "project_name" { + description = "Project name to prefix resources with" + type = string +} + +variable "assume_role_name" { + description = "Name of the IAM role that the lambda will assume in the target account" + type = string + default = "OrganizationAccountAccessRole" +} + +variable "cloudtrail_name_prefix" { + description = "Name Prefix of the cloudtrail name to delete" + type = string + default = "cloudtrail-" +} + +variable "error_not_found" { + description = "Error if the cloudtrail name/pattern is not found" + type = bool + default = true +} + +variable "event_bus_name" { + description = "Event bus name to create event rules in" + type = string + default = "default" +} + +variable "event_types" { + description = "Event types that will trigger this lambda" + type = set(string) + default = [ + "CreateAccountResult", + "InviteAccountToOrganization", + ] + + validation { + condition = alltrue([for event in var.event_types : contains(["CreateAccountResult", "InviteAccountToOrganization"], event)]) + error_message = "Supported event_types include only: CreateAccountResult, InviteAccountToOrganization" + } +} + +variable "dry_run" { + description = "Boolean toggle to control the dry-run mode of the lambda function" + type = bool + default = true +} + +variable "lambda" { + description = "Object of optional attributes passed on to the lambda module" + type = object({ + artifacts_dir = optional(string, "builds") + build_in_docker = optional(bool, false) + create_package = optional(bool, true) + ephemeral_storage_size = optional(number) + ignore_source_code_hash = optional(bool, true) + local_existing_package = optional(string) + memory_size = optional(number, 128) + recreate_missing_package = optional(bool, false) + runtime = optional(string, "python3.8") + s3_bucket = optional(string) + s3_existing_package = optional(map(string)) + s3_prefix = optional(string) + store_on_s3 = optional(bool, false) + timeout = optional(number, 300) + }) + default = {} +} + +variable "log_level" { + description = "Log level for lambda" + type = string + default = "INFO" + validation { + condition = contains(["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"], var.log_level) + error_message = "Valid values for log level are (CRITICAL, ERROR, WARNING, INFO, DEBUG)." + } +} + +variable "tags" { + description = "Tags for resource" + type = map(string) + default = {} +} diff --git a/versions.tf b/versions.tf new file mode 100644 index 0000000..2ab1e86 --- /dev/null +++ b/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.3" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.9" + } + } +} From 4d56753d9b5d4fc1dafd06f0040552bee85381f1 Mon Sep 17 00:00:00 2001 From: Dennis Carey Date: Fri, 11 Aug 2023 10:47:51 -0400 Subject: [PATCH 02/10] Add bumpversion and .github dependabot changes and remove template --- .bumpversion.cfg | 2 +- .github/dependabot.yml | 25 +++++++++++++++++++++++-- CHANGELOG.template.md | 13 ------------- 3 files changed, 24 insertions(+), 16 deletions(-) delete mode 100644 CHANGELOG.template.md diff --git a/.bumpversion.cfg b/.bumpversion.cfg index a5731e4..fab32b7 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.2.0 +current_version = 1.0.0 commit = True message = Bumps version to {new_version} tag = False diff --git a/.github/dependabot.yml b/.github/dependabot.yml index bb9ff6c..7618d5d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,6 +7,27 @@ updates: interval: weekly # Maintain dependencies for dockerfiles - package-ecosystem: docker - directory: / + directory: "/" schedule: - interval: weekly + interval: daily + open-pull-requests-limit: 10 + - package-ecosystem: pip + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 + - package-ecosystem: pip + directory: "/src" + schedule: + interval: daily + open-pull-requests-limit: 10 + - package-ecosystem: terraform + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 + - package-ecosystem: terraform + directory: "/tests/test_delete_default_cloudtrail" + schedule: + interval: daily + open-pull-requests-limit: 10 diff --git a/CHANGELOG.template.md b/CHANGELOG.template.md deleted file mode 100644 index c61f573..0000000 --- a/CHANGELOG.template.md +++ /dev/null @@ -1,13 +0,0 @@ -## {{ repo-name }} - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). - -### [{{Major.Minor.Patch}}](https://github.com/plus3it/{{RepoName}}/releases/tag/{{Major.Minor.Patch}}) - -**Released**: {{ YYYY.MM.DD }} - -**Summary**: - -* {{ Bulleted descriptions of enhancements, changes, or fixes }} From 102b9da8a867ecc3aa51824163c6b4d12a703282 Mon Sep 17 00:00:00 2001 From: Dennis Carey Date: Fri, 11 Aug 2023 10:51:23 -0400 Subject: [PATCH 03/10] Fix typo --- src/delete_default_cloudtrail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/delete_default_cloudtrail.py b/src/delete_default_cloudtrail.py index 775422c..00a819f 100644 --- a/src/delete_default_cloudtrail.py +++ b/src/delete_default_cloudtrail.py @@ -7,7 +7,7 @@ valid input: critical, error, warning, info (default), debug CLOUDTRAIL_NAME_PREFIX: cloudtrail name to delete (default: cloudtrail-) DRY_RUN: (optional): true or false, defaults to true - ASSUME_ROLE_NAME: Name of role sto assume + ASSUME_ROLE_NAME: Name of role to assume """ from argparse import ArgumentParser, RawDescriptionHelpFormatter import collections From 8fdb41c12867e0ffdc7c814f03ffe58711ad2c9c Mon Sep 17 00:00:00 2001 From: Dennis Carey Date: Mon, 14 Aug 2023 11:39:39 -0400 Subject: [PATCH 04/10] Update to latests gha test.yml --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9dbd6dc..906f7c4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,4 +8,4 @@ concurrency: jobs: test: - uses: plus3it/actions-workflows/.github/workflows/test.yml@86222127307c7f827e141bbc35cf0efb0e611648 + uses: plus3it/actions-workflows/.github/workflows/test.yml@93a9326e07945e5441d0fadef735563290edd729 From 31970c1689fd9f7fd24d21e32ffd60e169a0ad5f Mon Sep 17 00:00:00 2001 From: Dennis Carey Date: Thu, 31 Aug 2023 13:36:09 -0400 Subject: [PATCH 05/10] ignore missing import --- src/delete_default_cloudtrail.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/delete_default_cloudtrail.py b/src/delete_default_cloudtrail.py index 00a819f..724aab4 100644 --- a/src/delete_default_cloudtrail.py +++ b/src/delete_default_cloudtrail.py @@ -16,7 +16,9 @@ import sys import boto3 -from aws_assume_role_lib import assume_role, generate_lambda_session_name +from aws_assume_role_lib import ( # pylint: disable=import-error + assume_role, generate_lambda_session_name +) from botocore.exceptions import ClientError # Standard logging config From 6ad0c4efe3c90715e98a8968ed0b7cad4ea910a5 Mon Sep 17 00:00:00 2001 From: Dennis Carey Date: Thu, 31 Aug 2023 14:09:31 -0400 Subject: [PATCH 06/10] Use make pullrequest to address formatting --- src/delete_default_cloudtrail.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/delete_default_cloudtrail.py b/src/delete_default_cloudtrail.py index 724aab4..4455825 100644 --- a/src/delete_default_cloudtrail.py +++ b/src/delete_default_cloudtrail.py @@ -17,7 +17,8 @@ import boto3 from aws_assume_role_lib import ( # pylint: disable=import-error - assume_role, generate_lambda_session_name + assume_role, + generate_lambda_session_name, ) from botocore.exceptions import ClientError From bedfa9541fa510fbf27edf9d169e0d7f95cf8128 Mon Sep 17 00:00:00 2001 From: Dennis Carey Date: Thu, 31 Aug 2023 14:24:18 -0400 Subject: [PATCH 07/10] Create cloudtrail with minimum settings so moto can create it --- tests/test_delete_default_cloudtrail/cloudtrail/main.tf | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_delete_default_cloudtrail/cloudtrail/main.tf b/tests/test_delete_default_cloudtrail/cloudtrail/main.tf index 02b5d8b..bb561bd 100644 --- a/tests/test_delete_default_cloudtrail/cloudtrail/main.tf +++ b/tests/test_delete_default_cloudtrail/cloudtrail/main.tf @@ -5,8 +5,6 @@ data "aws_region" "current" {} resource "aws_cloudtrail" "this" { name = var.cloudtrail_name s3_bucket_name = aws_s3_bucket.this.id - s3_key_prefix = "prefix" - include_global_service_events = false tags = var.tags } From 2209b64587bb70fa4d81a31fa4404ebbf9616920 Mon Sep 17 00:00:00 2001 From: Dennis Carey Date: Thu, 31 Aug 2023 14:25:44 -0400 Subject: [PATCH 08/10] Run formatter --- tests/test_delete_default_cloudtrail/cloudtrail/main.tf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_delete_default_cloudtrail/cloudtrail/main.tf b/tests/test_delete_default_cloudtrail/cloudtrail/main.tf index bb561bd..8f2222f 100644 --- a/tests/test_delete_default_cloudtrail/cloudtrail/main.tf +++ b/tests/test_delete_default_cloudtrail/cloudtrail/main.tf @@ -3,9 +3,9 @@ data "aws_partition" "current" {} data "aws_region" "current" {} resource "aws_cloudtrail" "this" { - name = var.cloudtrail_name - s3_bucket_name = aws_s3_bucket.this.id - tags = var.tags + name = var.cloudtrail_name + s3_bucket_name = aws_s3_bucket.this.id + tags = var.tags } resource "aws_s3_bucket" "this" { From a5406cee7acdf5f8f1f511173adeaf7fd44c4a58 Mon Sep 17 00:00:00 2001 From: Dennis Carey Date: Fri, 8 Sep 2023 11:25:26 -0400 Subject: [PATCH 09/10] Update tardigrade-ci to pull in localstack fixes --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index b35a912..24432c6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1 +1 @@ -FROM plus3it/tardigrade-ci:0.24.11 +FROM plus3it/tardigrade-ci:0.24.13 From e0690b9e2082bce46890d8b04c6e08c6d35023b1 Mon Sep 17 00:00:00 2001 From: Dennis Carey Date: Fri, 8 Sep 2023 12:53:08 -0400 Subject: [PATCH 10/10] Use only moto endpoints --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index 634033f..f38910c 100644 --- a/Makefile +++ b/Makefile @@ -1 +1,3 @@ +export ONLY_MOTO := true + include $(shell test -f .tardigrade-ci || curl -sSL -o .tardigrade-ci "https://raw.githubusercontent.com/plus3it/tardigrade-ci/master/bootstrap/Makefile.bootstrap"; echo .tardigrade-ci)