diff --git a/docs/config-contrib.md b/docs/config-contrib.md new file mode 100644 index 00000000..9e648c72 --- /dev/null +++ b/docs/config-contrib.md @@ -0,0 +1,107 @@ +# Config Contributions + +## Community Presets + +These are a collection of presets from the community. + +!!! warning + These presets are built from feedback from the community, they are not routinely tested. Use at your own risk. + +### Filter SSO Resources + +This is a preset to filter out AWS SSO resources. + +```yaml +presets: + sso: + filters: + IAMSAMLProvider: + - type: "regex" + value: "AWSSSO_.*_DO_NOT_DELETE" + IAMRole: + - type: "glob" + value: "AWSReservedSSO_*" + IAMRolePolicyAttachment: + - type: "glob" + value: "AWSReservedSSO_*" +``` + +### Filter Control Tower + +This is a preset to filter out AWS Control Tower resources. + +```yaml +presets: + controltower: + filters: + CloudTrailTrail: + - type: "contains" + value: "aws-controltower" + CloudWatchEventsRule: + - type: "contains" + value: "aws-controltower" + - property: "Name" + type: glob + value: "AWSControlTower*" + EC2VPCEndpoint: + - type: "contains" + value: "aws-controltower" + EC2VPC: + - type: "contains" + value: "aws-controltower" + OpsWorksUserProfile: + - type: "contains" + value: "AWSControlTowerExecution" + CloudWatchLogsLogGroup: + - type: "contains" + value: "aws-controltower" + - type: "contains" + value: "AWSControlTowerBP" + CloudWatchEventsTarget: + - type: "contains" + value: "aws-controltower" + - type: "glob" + value: "Rule: AWSControlTower*" + SNSSubscription: + - type: "contains" + value: "aws-controltower" + SNSTopic: + - type: "contains" + value: "aws-controltower" + EC2Subnet: + - type: "contains" + value: "aws-controltower" + ConfigServiceDeliveryChannel: + - type: "contains" + value: "aws-controltower" + ConfigServiceConfigurationRecorder: + - type: "contains" + value: "aws-controltower" + CloudFormationStack: + - type: "contains" + value: "AWSControlTower" + EC2RouteTable: + - type: "contains" + value: "aws-controltower" + LambdaFunction: + - type: "contains" + value: "aws-controltower" + EC2DHCPOption: + - type: "contains" + value: "aws-controltower" + IAMRole: + - type: "contains" + value: "aws-controltower" + - type: "contains" + value: "AWSControlTower" + IAMRolePolicyAttachment: + - type: "contains" + value: "aws-controltower" + - type: "contains" + value: "AWSControlTower" + IAMRolePolicy: + - type: "contains" + value: "aws-controltower" + - type: glob + value: "AWSReservedSSO_*" +``` \ No newline at end of file diff --git a/docs/resources/ec2-image.md b/docs/resources/ec2-image.md new file mode 100644 index 00000000..e2ef55e7 --- /dev/null +++ b/docs/resources/ec2-image.md @@ -0,0 +1,36 @@ +# EC2 Image + +This will remove all EC2 Images (AMI) in an AWS account. + +## Resource + +```text +EC2Image +``` + +## Settings + +- `IncludeDisabled` +- `IncludeDeprecated` +- `DisableDeregistrationProtection` + +### IncludeDisabled + +This will include any EC2 Images (AMI) that are disabled in the deletion process. By default, disabled images are excluded +from the discovery process. + +Default is `false`. + +### IncludeDeprecated + +This will include any EC2 Images (AMI) that are deprecated in the deletion process. By default, deprecated images are excluded +from the discovery process. + +Default is `false`. + +### DisableDeregistrationProtection + +This will disable the deregistration protection on the EC2 Image (AMI) prior to deletion. By default, deregistration protection +is not disabled. + +Default is `false`. diff --git a/docs/resources/iam-role.md b/docs/resources/iam-role.md index efbabdd2..6669c372 100644 --- a/docs/resources/iam-role.md +++ b/docs/resources/iam-role.md @@ -2,6 +2,12 @@ This will remove all IAM Roles an AWS account. +## Resource + +```text +IAMRole +``` + ## Settings - `IncludeServiceLinkedRoles` diff --git a/docs/resources/overview.md b/docs/resources/overview.md new file mode 100644 index 00000000..f0a1f6f9 --- /dev/null +++ b/docs/resources/overview.md @@ -0,0 +1,5 @@ +# Resources Overview + +This is the start of the documentation for all resources handled by aws-nuke. Eventually each resource will have its own +page with detailed information on how to use it, what settings are available, and what the resource does. + diff --git a/docs/resources/s3-bucket.md b/docs/resources/s3-bucket.md index fc4ffd2c..8925361f 100644 --- a/docs/resources/s3-bucket.md +++ b/docs/resources/s3-bucket.md @@ -13,6 +13,12 @@ This will remove all S3 buckets from an AWS account. The following actions are p - This will include bypassing any Object Lock governance retention settings if the `BypassGovernanceRetention` setting is set to `true` +## Resource + +```text +S3Bucket +``` + ## Settings - `BypassGovernanceRetention` diff --git a/docs/resources/s3-object.md b/docs/resources/s3-object.md new file mode 100644 index 00000000..ac83310c --- /dev/null +++ b/docs/resources/s3-object.md @@ -0,0 +1,22 @@ +# S3Object + +!!! warning + **You should exclude this resource by default.** Not doing so can lead to deadlocks and hung runs of the tool. In + the next major version of aws-nuke, this resource will be excluded by default. + +!!! important + This resource is **NOT** required to remove a [S3Bucket](./s3-bucket.md). The `S3Bucket` resource will remove all + objects in the bucket as part of the deletion process using a batch removal process. + +This removes all objects from S3 buckets in an AWS account while retaining the S3 bucket itself. This resource is +useful if you want to remove a single object from a bucket, or a subset of objects without removing the entire bucket. + +## Resource + +```text +S3Object +``` + +## Settings + +**No settings available.** \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index f7846d78..a18f814f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -91,10 +91,14 @@ nav: - Presets: config-presets.md - Custom Endpoints: config-custom-endpoints.md - Migration Guide: config-migration.md + - Examples & Presets: config-contrib.md - Resources: + - Overview: resources/overview.md - Cognito User Pool: resources/cognito-user-pool.md + - EC2 Image: resources/ec2-image.md - IAM Role: resources/iam-role.md - S3 Bucket: resources/s3-bucket.md + - S3 Object: resources/s3-object.md - Development: - Overview: development.md - Contributing: contributing.md diff --git a/resources/apigateway-api-key.go b/resources/apigateway-api-key.go index cc53b1f1..e953f09e 100644 --- a/resources/apigateway-api-key.go +++ b/resources/apigateway-api-key.go @@ -4,6 +4,8 @@ import ( "context" "time" + "go.uber.org/ratelimit" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/apigateway" @@ -16,6 +18,12 @@ import ( const APIGatewayAPIKeyResource = "APIGatewayAPIKey" +// Rate limit to avoid throttling when deleting API Gateway Rest APIs +// The API Gateway Delete Rest API has a limit of 1 request per 30 seconds for each account +// https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html +// Note: due to time drift, set to 31 seconds to be safe. +var deleteAPIKeyLimit = ratelimit.New(1, ratelimit.Per(32*time.Second)) + func init() { registry.Register(®istry.Registration{ Name: APIGatewayAPIKeyResource, @@ -72,6 +80,8 @@ type APIGatewayAPIKey struct { } func (r *APIGatewayAPIKey) Remove(_ context.Context) error { + deleteAPIKeyLimit.Take() + _, err := r.svc.DeleteApiKey(&apigateway.DeleteApiKeyInput{ ApiKey: r.apiKey, }) diff --git a/resources/apigateway-restapis.go b/resources/apigateway-restapis.go index 9be96efd..2d3bf148 100644 --- a/resources/apigateway-restapis.go +++ b/resources/apigateway-restapis.go @@ -4,6 +4,8 @@ import ( "context" "time" + "go.uber.org/ratelimit" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/apigateway" @@ -16,6 +18,12 @@ import ( const APIGatewayRestAPIResource = "APIGatewayRestAPI" +// Rate limit to avoid throttling when deleting API Gateway Rest APIs +// The API Gateway Delete Rest API has a limit of 1 request per 30 seconds for each account +// https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html +// Note: due to time drift, set to 31 seconds to be safe. +var deleteRestAPILimit = ratelimit.New(1, ratelimit.Per(32*time.Second)) + func init() { registry.Register(®istry.Registration{ Name: APIGatewayRestAPIResource, @@ -73,6 +81,8 @@ func (l *APIGatewayRestAPILister) List(_ context.Context, o interface{}) ([]reso } func (f *APIGatewayRestAPI) Remove(_ context.Context) error { + deleteRestAPILimit.Take() + _, err := f.svc.DeleteRestApi(&apigateway.DeleteRestApiInput{ RestApiId: f.restAPIID, }) diff --git a/resources/ec2-eip.go b/resources/ec2-eip.go index 6cf050a3..f920ff48 100644 --- a/resources/ec2-eip.go +++ b/resources/ec2-eip.go @@ -3,8 +3,6 @@ package resources import ( "context" - "github.com/gotidy/ptr" - "github.com/aws/aws-sdk-go/service/ec2" "github.com/ekristen/libnuke/pkg/registry" @@ -40,10 +38,10 @@ func (l *EC2AddressLister) List(_ context.Context, o interface{}) ([]resource.Re resources := make([]resource.Resource, 0) for _, out := range resp.Addresses { resources = append(resources, &EC2Address{ - svc: svc, - eip: out, - id: ptr.ToString(out.AllocationId), - ip: ptr.ToString(out.PublicIp), + svc: svc, + AllocationID: out.AllocationId, + PublicIP: out.PublicIp, + Tags: out.Tags, }) } @@ -51,15 +49,17 @@ func (l *EC2AddressLister) List(_ context.Context, o interface{}) ([]resource.Re } type EC2Address struct { - svc *ec2.EC2 - eip *ec2.Address - id string - ip string + svc *ec2.EC2 + AllocationID *string + PublicIP *string + NetworkBorderGroup *string + Tags []*ec2.Tag } -func (e *EC2Address) Remove(_ context.Context) error { - _, err := e.svc.ReleaseAddress(&ec2.ReleaseAddressInput{ - AllocationId: &e.id, +func (r *EC2Address) Remove(_ context.Context) error { + _, err := r.svc.ReleaseAddress(&ec2.ReleaseAddressInput{ + AllocationId: r.AllocationID, + NetworkBorderGroup: r.NetworkBorderGroup, }) if err != nil { return err @@ -68,15 +68,10 @@ func (e *EC2Address) Remove(_ context.Context) error { return nil } -func (e *EC2Address) Properties() types.Properties { - properties := types.NewProperties() - for _, tagValue := range e.eip.Tags { - properties.SetTag(tagValue.Key, tagValue.Value) - } - properties.Set("AllocationID", e.id) - return properties +func (r *EC2Address) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) } -func (e *EC2Address) String() string { - return e.ip +func (r *EC2Address) String() string { + return *r.PublicIP } diff --git a/resources/kms-key.go b/resources/kms-key.go index 043824e5..0b5f46cb 100644 --- a/resources/kms-key.go +++ b/resources/kms-key.go @@ -98,6 +98,17 @@ func (l *KMSKeyLister) List(_ context.Context, o interface{}) ([]resource.Resour } } + keyAliases, err := svc.ListAliases(&kms.ListAliasesInput{ + KeyId: key.KeyId, + }) + if err != nil { + logrus.WithError(err).Error("unable to list aliases") + } + + if len(keyAliases.Aliases) > 0 { + kmsKey.Alias = keyAliases.Aliases[0].AliasName + } + resources = append(resources, kmsKey) } @@ -118,6 +129,7 @@ type KMSKey struct { ID *string State *string Manager *string + Alias *string Tags []*kms.Tag } diff --git a/resources/kms-key_mock_test.go b/resources/kms-key_mock_test.go index 0d4c5df8..a9fd1989 100644 --- a/resources/kms-key_mock_test.go +++ b/resources/kms-key_mock_test.go @@ -56,6 +56,14 @@ func Test_Mock_KMSKey_List(t *testing.T) { }, ) + mockKMS.EXPECT().ListAliases(&kms.ListAliasesInput{ + KeyId: aws.String("test-key-id"), + }).Return(&kms.ListAliasesOutput{ + Aliases: []*kms.AliasListEntry{ + {AliasName: aws.String("alias/test-key-id")}, + }, + }, nil) + lister := KMSKeyLister{ mockSvc: mockKMS, } @@ -123,6 +131,14 @@ func Test_Mock_KMSKey_List_WithAccessDenied(t *testing.T) { }, ) + mockKMS.EXPECT().ListAliases(&kms.ListAliasesInput{ + KeyId: aws.String("test-key-id-1"), + }).Return(&kms.ListAliasesOutput{ + Aliases: []*kms.AliasListEntry{ + {AliasName: aws.String("alias/test-key-id-1")}, + }, + }, nil) + lister := KMSKeyLister{ mockSvc: mockKMS, } @@ -180,6 +196,7 @@ func Test_Mock_KMSKey_Properties(t *testing.T) { ID: ptr.String("test-key-id"), State: ptr.String(kms.KeyStateEnabled), Manager: ptr.String(kms.KeyManagerTypeCustomer), + Alias: ptr.String("alias/test-key-id"), Tags: []*kms.Tag{ {TagKey: aws.String("Environment"), TagValue: aws.String("Test")}, }, @@ -189,6 +206,7 @@ func Test_Mock_KMSKey_Properties(t *testing.T) { assert.Equal(t, kms.KeyStateEnabled, kmsKey.Properties().Get("State")) assert.Equal(t, kms.KeyManagerTypeCustomer, kmsKey.Properties().Get("Manager")) assert.Equal(t, "Test", kmsKey.Properties().Get("tag:Environment")) + assert.Equal(t, "alias/test-key-id", kmsKey.Properties().Get("Alias")) } func Test_Mock_KMSKey_Remove(t *testing.T) { diff --git a/resources/neptune-instances.go b/resources/neptune-instance.go similarity index 54% rename from resources/neptune-instances.go rename to resources/neptune-instance.go index 8bf1ba01..b425704c 100644 --- a/resources/neptune-instances.go +++ b/resources/neptune-instance.go @@ -3,11 +3,14 @@ package resources import ( "context" + "github.com/sirupsen/logrus" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/neptune" "github.com/ekristen/libnuke/pkg/registry" "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" "github.com/ekristen/aws-nuke/v3/pkg/nuke" ) @@ -32,6 +35,12 @@ func (l *NeptuneInstanceLister) List(_ context.Context, o interface{}) ([]resour params := &neptune.DescribeDBInstancesInput{ MaxRecords: aws.Int64(100), + Filters: []*neptune.Filter{ + { + Name: aws.String("engine"), + Values: []*string{aws.String("neptune")}, + }, + }, } for { @@ -41,9 +50,22 @@ func (l *NeptuneInstanceLister) List(_ context.Context, o interface{}) ([]resour } for _, dbInstance := range output.DBInstances { + var dbTags []*neptune.Tag + tags, err := svc.ListTagsForResource(&neptune.ListTagsForResourceInput{ + ResourceName: dbInstance.DBInstanceArn, + }) + if err != nil { + logrus.WithError(err).Warn("failed to list tags for resource") + } + if tags.TagList != nil { + dbTags = tags.TagList + } + resources = append(resources, &NeptuneInstance{ - svc: svc, - ID: dbInstance.DBInstanceIdentifier, + svc: svc, + ID: dbInstance.DBInstanceIdentifier, + Name: dbInstance.DBName, + Tags: dbTags, }) } @@ -58,19 +80,25 @@ func (l *NeptuneInstanceLister) List(_ context.Context, o interface{}) ([]resour } type NeptuneInstance struct { - svc *neptune.Neptune - ID *string + svc *neptune.Neptune + ID *string + Name *string + Tags []*neptune.Tag } -func (f *NeptuneInstance) Remove(_ context.Context) error { - _, err := f.svc.DeleteDBInstance(&neptune.DeleteDBInstanceInput{ - DBInstanceIdentifier: f.ID, +func (r *NeptuneInstance) Remove(_ context.Context) error { + _, err := r.svc.DeleteDBInstance(&neptune.DeleteDBInstanceInput{ + DBInstanceIdentifier: r.ID, SkipFinalSnapshot: aws.Bool(true), }) return err } -func (f *NeptuneInstance) String() string { - return *f.ID +func (r *NeptuneInstance) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} + +func (r *NeptuneInstance) String() string { + return *r.ID } diff --git a/resources/opensearchservice-pipeline.go b/resources/opensearchservice-pipeline.go new file mode 100644 index 00000000..89e14fe6 --- /dev/null +++ b/resources/opensearchservice-pipeline.go @@ -0,0 +1,83 @@ +package resources + +import ( + "context" + "time" + + "github.com/aws/aws-sdk-go/service/osis" + + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/resource" + "github.com/ekristen/libnuke/pkg/types" + + "github.com/ekristen/aws-nuke/v3/pkg/nuke" +) + +const OSPipelineResource = "OSPipeline" + +func init() { + registry.Register(®istry.Registration{ + Name: OSPipelineResource, + Scope: nuke.Account, + Lister: &OSPipelineLister{}, + }) +} + +type OSPipelineLister struct{} + +func (l *OSPipelineLister) List(_ context.Context, o interface{}) ([]resource.Resource, error) { + opts := o.(*nuke.ListerOpts) + var resources []resource.Resource + + svc := osis.New(opts.Session) + + params := &osis.ListPipelinesInput{} + + for { + res, err := svc.ListPipelines(params) + if err != nil { + return nil, err + } + + for _, p := range res.Pipelines { + resources = append(resources, &OSPipeline{ + svc: svc, + Name: p.PipelineName, + Tags: p.Tags, + Status: p.Status, + CreatedAt: p.CreatedAt, + }) + } + + if res.NextToken == nil { + break + } + + params.NextToken = res.NextToken + } + + return resources, nil +} + +type OSPipeline struct { + svc *osis.OSIS + Name *string + Status *string + CreatedAt *time.Time + Tags []*osis.Tag +} + +func (r *OSPipeline) Remove(_ context.Context) error { + _, err := r.svc.DeletePipeline(&osis.DeletePipelineInput{ + PipelineName: r.Name, + }) + return err +} + +func (r *OSPipeline) Properties() types.Properties { + return types.NewPropertiesFromStruct(r) +} + +func (r *OSPipeline) String() string { + return *r.Name +} diff --git a/resources/secretsmanager-secret.go b/resources/secretsmanager-secret.go index 16bee586..c693ce36 100644 --- a/resources/secretsmanager-secret.go +++ b/resources/secretsmanager-secret.go @@ -2,6 +2,8 @@ package resources import ( "context" + "errors" + "regexp" "strings" "github.com/gotidy/ptr" @@ -19,6 +21,9 @@ import ( const SecretsManagerSecretResource = "SecretsManagerSecret" +var managedRegex = regexp.MustCompile("^([a-z-]+)!.*$") +var errAWSManaged = errors.New("cannot delete AWS managed secret") + func init() { registry.Register(®istry.Registration{ Name: SecretsManagerSecretResource, @@ -128,6 +133,20 @@ func (r *SecretsManagerSecret) Remove(_ context.Context) error { return err } +func (r *SecretsManagerSecret) Filter() error { + if managedRegex.MatchString(*r.Name) { + return errAWSManaged + } + + for _, tag := range r.tags { + if *tag.Key == "aws:secretsmanager:owningService" { + return errAWSManaged + } + } + + return nil +} + func (r *SecretsManagerSecret) Properties() types.Properties { properties := types.NewProperties() properties.Set("PrimaryRegion", r.PrimaryRegion)