diff --git a/.tools/test/stacks/nuke/typescript/Dockerfile b/.tools/test/stacks/nuke/typescript/Dockerfile new file mode 100644 index 00000000000..d451651bf7c --- /dev/null +++ b/.tools/test/stacks/nuke/typescript/Dockerfile @@ -0,0 +1,12 @@ +FROM ghcr.io/ekristen/aws-nuke:v3.42.0 +ENV AWS_SDK_LOAD_CONFIG=1 \ + AWS_DEBUG=true +USER root +RUN apk add --no-cache \ + python3 \ + py3-pip \ + aws-cli +COPY nuke_generic_config.yaml /nuke_generic_config.yaml +COPY --chmod=755 run.sh /run.sh +USER aws-nuke +ENTRYPOINT ["/run.sh"] diff --git a/.tools/test/stacks/nuke/typescript/README.md b/.tools/test/stacks/nuke/typescript/README.md new file mode 100644 index 00000000000..02cb6b7fc01 --- /dev/null +++ b/.tools/test/stacks/nuke/typescript/README.md @@ -0,0 +1,48 @@ + +# aws-nuke for Weathertop + +[aws-nuke](https://github.com/ekristen/aws-nuke) is an open-source tool that deletes non-default resources in a provided AWS account. It's implemented here in this directory using Cloud Development Kit (CDK) code that deploys the [official aws-nuke image](https://github.com/ekristen/aws-nuke/pkgs/container/aws-nuke) to an AWS Lambda function. + +## ⚠ Important +This is a very destructive tool! It should not be deployed without fully understanding the impact it will have on your AWS accounts. +Please use caution and configure this tool to delete unused resources only in your lower test/sandbox environment accounts. + +## Overview + +This CDK stack is defined in [account_nuker.ts](account_nuker.ts). It includes: +- A Docker-based Lambda function with ARM64 architecture and 1GB memory +- An IAM role with administrative permissions for the Lambda's nuking function +- An EventBridge rule that triggers the function every Sunday at midnight + +More specifically, this Lambda function is built from a [Dockerfile](Dockerfile) and runs with a 15-minute timeout. It contains a [nuke_generic_config.yml](nuke_generic_config.yaml) config and executes a [run.sh](run.sh) when invoked every Sunday at midnight UTC. + +![infrastructure-overview](nuke-overview.png) + +## Prerequisites +1. **Non-Prod AWS Account Alias**: A non-prod account alias must exist in target account. Set the alias by running `python create_account_alias.py weathertop-test` or following [these instructions](https://docs.aws.amazon.com/IAM/latest/UserGuide/account-alias-create.html). + +## Setup and Installation +For multi-account deployments, please use the [deploy.py](../../../DEPLOYMENT.md#option-1-using-deploypy) script. + +For single-account deployment, you can just run: +```sh +cdk bootstrap && cdk deploy +``` + +Note a successful stack creation, e.g.: + +```bash +NukeStack: success: Published 956fbd116734e79edb987e767fe7f45d0b97e2123456789109103f80ba4c1:123456789101-us-east-1 +Stack undefined +NukeStack: deploying... [1/1] +NukeStack: creating CloudFormation changeset... + + ✅ NukeStack + +✨ Deployment time: 27.93s + +Stack ARN: +arn:aws:cloudformation:us-east-1:123456789101:stack/NukeStack/9835cc20-d358-11ef-bccf-123407dc82dd + +✨ Total time: 33.24s +``` diff --git a/.tools/test/stacks/nuke/typescript/account_nuker.ts b/.tools/test/stacks/nuke/typescript/account_nuker.ts new file mode 100644 index 00000000000..a5546561556 --- /dev/null +++ b/.tools/test/stacks/nuke/typescript/account_nuker.ts @@ -0,0 +1,62 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as cdk from 'aws-cdk-lib'; +import * as events from 'aws-cdk-lib/aws-events'; +import * as targets from 'aws-cdk-lib/aws-events-targets'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as path from 'path'; +import * as lambda from "aws-cdk-lib/aws-lambda"; +import { Duration, Stack, StackProps } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { DockerImageCode, DockerImageFunction } from 'aws-cdk-lib/aws-lambda'; + +export interface NukeStackProps extends cdk.StackProps { + awsNukeDryRunFlag?: string; + awsNukeVersion?: string; + owner?: string; +} + +class NukeStack extends cdk.Stack { + private readonly nukeLambdaRole: iam.Role; + + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + + // Lambda Function role + this.nukeLambdaRole = new iam.Role(this, 'NukeLambdaRole', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('AdministratorAccess'), + ], + }); + + // Create the Lambda function + const lambdaFunction = new DockerImageFunction(this, 'docker-lambda-function', { + functionName: "docker-lambda-fn", + code: DockerImageCode.fromImageAsset(path.join(__dirname)), + memorySize: 1024, + timeout: Duration.minutes(15), + architecture: lambda.Architecture.ARM_64, + description: "This is dockerized AWS Lambda function", + role: this.nukeLambdaRole + }); + + // Create EventBridge rule to trigger the Lambda function weekly + const rule = new events.Rule(this, 'WeeklyTriggerRule', { + schedule: events.Schedule.expression('cron(0 0 ? * SUN *)') // Runs at 00:00 every Sunday + }); + + // Add the Lambda function as a target for the EventBridge rule + rule.addTarget(new targets.LambdaFunction(lambdaFunction)); + } +} + +const app = new cdk.App(); +new NukeStack(app, 'NukeStack', { + env: { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION, + }, + terminationProtection: true +}); diff --git a/.tools/test/stacks/nuke/typescript/cdk.json b/.tools/test/stacks/nuke/typescript/cdk.json new file mode 100644 index 00000000000..cb6f4525503 --- /dev/null +++ b/.tools/test/stacks/nuke/typescript/cdk.json @@ -0,0 +1,81 @@ +{ + "app": "npx ts-node --prefer-ts-exts account_nuker.ts", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, + "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, + "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, + "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, + "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, + "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, + "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true, + "cdk-migrate": true + } +} diff --git a/.tools/test/stacks/nuke/typescript/create_account_alias.py b/.tools/test/stacks/nuke/typescript/create_account_alias.py new file mode 100644 index 00000000000..5be4b1d856b --- /dev/null +++ b/.tools/test/stacks/nuke/typescript/create_account_alias.py @@ -0,0 +1,115 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This module is used to create an AWS account alias, which is required by the deploy.py script. + +It provides a function to create an account alias using the AWS CLI, as this specific +operation is not supported by the AWS CDK. +""" + +import logging +import re +import subprocess + +logger = logging.getLogger(__name__) + + +def _is_valid_alias(alias_name: str) -> bool: + """ + Check if the provided alias name is valid according to AWS rules. + + AWS account alias must be unique and must be between 3 and 63 characters long. + Valid characters are a-z, 0-9 and '-'. + + Args: + alias_name (str): The alias name to validate. + + Returns: + bool: True if the alias is valid, False otherwise. + """ + pattern = r"^[a-z0-9](([a-z0-9]|-){0,61}[a-z0-9])?$" + return bool(re.match(pattern, alias_name)) and 3 <= len(alias_name) <= 63 + + +def _log_aws_cli_version() -> None: + """ + Log the version of the AWS CLI installed on the system. + """ + try: + result = subprocess.run(["aws", "--version"], capture_output=True, text=True) + logger.info(f"AWS CLI version: {result.stderr.strip()}") + except Exception as e: + logger.warning(f"Unable to determine AWS CLI version: {str(e)}") + + +def create_account_alias(alias_name: str) -> None: + """ + Create a new account alias with the given name. + + This function exists because the CDK does not support the specific + CreateAccountAliases API call. It attempts to create an account alias + using the AWS CLI and logs the result. + + If the account alias is created successfully, it logs a success message. + If the account alias already exists, it logs a message indicating that. + If there is any other error, it logs the error message. + + Args: + alias_name (str): The desired name for the account alias. + """ + # Log AWS CLI version when the function is called + _log_aws_cli_version() + + if not _is_valid_alias(alias_name): + logger.error( + f"Invalid alias name '{alias_name}'. It must be between 3 and 63 characters long and contain only lowercase letters, numbers, and hyphens." + ) + return + + command = ["aws", "iam", "create-account-alias", "--account-alias", alias_name] + + try: + subprocess.run( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=True, + ) + logger.info(f"Account alias '{alias_name}' created successfully.") + except subprocess.CalledProcessError as e: + if "EntityAlreadyExists" in e.stderr: + logger.info(f"Account alias '{alias_name}' already exists.") + elif "AccessDenied" in e.stderr: + logger.error( + f"Access denied when creating account alias '{alias_name}'. Check your AWS credentials and permissions." + ) + elif "ValidationError" in e.stderr: + logger.error( + f"Validation error when creating account alias '{alias_name}'. The alias might not meet AWS requirements." + ) + else: + logger.error(f"Error creating account alias '{alias_name}': {e.stderr}") + except Exception as e: + logger.error( + f"Unexpected error occurred while creating account alias '{alias_name}': {str(e)}" + ) + + +if __name__ == "__main__": + import argparse + + # Set up logging + logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" + ) + + # Create argument parser + parser = argparse.ArgumentParser(description="Create an AWS account alias") + parser.add_argument("alias", help="The alias name for the AWS account") + + # Parse arguments + args = parser.parse_args() + + # Call the function with the provided alias + create_account_alias(args.alias) diff --git a/.tools/test/stacks/nuke/typescript/nuke-architecture.jpg b/.tools/test/stacks/nuke/typescript/nuke-architecture.jpg new file mode 100644 index 00000000000..c5a69c71509 Binary files /dev/null and b/.tools/test/stacks/nuke/typescript/nuke-architecture.jpg differ diff --git a/.tools/test/stacks/nuke/typescript/nuke_generic_config.yaml b/.tools/test/stacks/nuke/typescript/nuke_generic_config.yaml new file mode 100644 index 00000000000..09fc355802b --- /dev/null +++ b/.tools/test/stacks/nuke/typescript/nuke_generic_config.yaml @@ -0,0 +1,156 @@ +regions: + - us-east-1 + +blocklist: + - 1234567890 + +resource-types: + excludes: + - ACMCertificate + - AWSBackupPlan + - AWSBackupRecoveryPoint + - AWSBackupSelection + - AWSBackupVault + - AWSBackupVaultAccessPolicy + - CloudTrailTrail + - CloudWatchEventsTarget + - CodeCommitRepository + - CodeStarProject + - ConfigServiceConfigRule + - ECRRepository + - EC2Address + - EC2ClientVpnEndpoint + - EC2ClientVpnEndpointAttachment + - EC2CustomerGateway + - EC2DHCPOption + - EC2DefaultSecurityGroupRule + - EC2EgressOnlyInternetGateway + - EC2InternetGateway + - EC2InternetGatewayAttachment + - EC2KeyPair + - EC2NetworkACL + - EC2NetworkInterface + - EC2RouteTable + - EC2SecurityGroup + - EC2Subnet + - EC2VPC + - EC2VPCEndpoint + - IAMGroup + - IAMGroupPolicy + - IAMGroupPolicyAttachment + - IAMInstanceProfile + - IAMInstanceProfileRole + - IAMLoginProfile + - IAMOpenIDConnectProvider + - IAMPolicy + - IAMRole + - IAMRolePolicy + - IAMRolePolicyAttachment + - IAMSAMLProvider + - IAMServerCertificate + - IAMServiceSpecificCredential + - IAMSigningCertificate + - IAMUser + - IAMUserAccessKey + - IAMUserGroupAttachment + - IAMUserPolicy + - IAMUserPolicyAttachment + - IAMUserSSHPublicKey + - IAMVirtualMFADevice + - KMSAlias + - KMSKey + - Route53HostedZone + - Route53ResourceRecordSet + - S3Bucket + - S3Object + - SecretsManagerSecret + - SQSQueue + - SSMParameter + +accounts: + AWSACCOUNTID: + filters: + EC2VPC: + - property: IsDefault + value: "true" + EC2DHCPOption: + - property: DefaultVPC + value: "true" + EC2InternetGateway: + - property: DefaultVPC + value: "true" + EC2InternetGatewayAttachment: + - property: DefaultVPC + value: "true" + EC2Subnet: + - property: DefaultVPC + value: "true" + EC2RouteTable: + - property: DefaultVPC + value: "true" + EC2DefaultSecurityGroupRule: + - property: SecurityGroupId + type: glob + value: "*" + LambdaEventSourceMapping: + - property: "EventSourceArn" + type: "glob" + value: "^(PluginStack|NukeStack)*$" + - property: "FunctionArn" + type: "glob" + value: "^(PluginStack|NukeStack)*$" + LambdaPermission: + - property: "name" + type: "glob" + value: "^(PluginStack|NukeStack)*$" + GuardDutyDetector: + - property: DetectorID + type: glob + value: "*" + CloudWatchEventsRule: + - type: regex + value: "^Rule: (AwsSecurity.*)$" + CloudWatchEventsTarget: + - type: regex + value: "^Rule: (AwsSecurity.*)$" + CloudWatchLogsLogGroup: + - type: regex + value: "^.*$" + ConfigServiceDeliveryChannel: + - "default" + ConfigServiceConfigRule: + - type: regex + value: "^(managed-ec2-patch-compliance|ec2-managed-by-systems-manager-REMEDIATE)$" + S3Bucket: + - property: Name + type: regex + value: "^(cdktoolkit-stagingbucket-.*|aws-nuke.*)$" + S3Object: + - property: Bucket + type: regex + value: "^(cdktoolkit-stagingbucket-.*|aws-nuke.*)$" + ConfigServiceConfigurationRecorder: + - "MainRecorder" + CloudFormationStack: + - property: Name + type: regex + value: "^(CDKToolkit)$" + - property: Name + type: regex + value: "^(PluginStack|NukeStack)*$" + IAMPolicy: + - property: Name + type: regex + value: "^(ConfigAccessPolicy|ResourceConfigurationCollectorPolicy|CloudFormationRefereeService|EC2CapacityReservationService|AwsSecurit.*AuditPolicy)$" + IAMRole: + - property: Name + type: regex + value: "^(AWSServiceRoleFor.*|Admin|ReadOnly|InternalAuditInternal|EC2CapacityReservationService|AccessAnalyzerTrustedService|AwsSecurit.*Audit|AWS.*Audit)$" + IAMRolePolicy: + - property: role:RoleName + type: regex + value: "^(AccessAnalyzerTrustedService|AwsSecurit.*Audit)$" + IAMRolePolicyAttachment: + - property: RoleName + type: regex + value: "^(Admin|ReadOnly|AWSServiceRoleFor.*|InternalAuditInternal|EC2CapacityReservationService|AWSVAPTAudit|AwsSecurit.*Audit)$" diff --git a/.tools/test/stacks/nuke/typescript/package.json b/.tools/test/stacks/nuke/typescript/package.json new file mode 100644 index 00000000000..8353504f81d --- /dev/null +++ b/.tools/test/stacks/nuke/typescript/package.json @@ -0,0 +1,27 @@ +{ + "name": "account_nuker", + "version": "0.1.0", + "bin": { + "nuke_cleanser": "account_nuker.ts" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest", + "cdk": "cdk" + }, + "devDependencies": { + "@types/jest": "^29.5.12", + "@types/node": "22.5.4", + "aws-cdk": "2.164.1", + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "typescript": "~5.6.2" + }, + "dependencies": { + "aws-cdk-lib": "^2.164.1", + "constructs": "^10.4.2", + "source-map-support": "^0.5.21" + } +} diff --git a/.tools/test/stacks/nuke/typescript/run.sh b/.tools/test/stacks/nuke/typescript/run.sh new file mode 100755 index 00000000000..649d8857ba1 --- /dev/null +++ b/.tools/test/stacks/nuke/typescript/run.sh @@ -0,0 +1,15 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +#!/bin/sh + +# Get AWS account ID +AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) +echo "AWS Account ID: $AWS_ACCOUNT_ID" + +# Copy the config file to /tmp and inject Account ID +echo "Copying & updating config file..." +cp /nuke_generic_config.yaml /tmp/nuke_config.yaml +sed -i "s/AWSACCOUNTID/$AWS_ACCOUNT_ID/g" /tmp/nuke_config.yaml + +echo "Running aws-nuke command:" +/usr/local/bin/aws-nuke run --config /tmp/nuke_config.yaml --force --max-wait-retries --no-dry-run 10 2>&1 diff --git a/.tools/test/stacks/nuke/typescript/tsconfig.json b/.tools/test/stacks/nuke/typescript/tsconfig.json new file mode 100644 index 00000000000..aaa7dc510f1 --- /dev/null +++ b/.tools/test/stacks/nuke/typescript/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": [ + "es2020", + "dom" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +}