From dd910da98d20b6a84ce0f4479a09cd2fe06baf52 Mon Sep 17 00:00:00 2001 From: Erik Kristensen Date: Thu, 22 Feb 2024 21:25:41 -0700 Subject: [PATCH] feat: new explain commands for debugging and informational purposes (#86) * feat: account command to show what you are authenticated against and how * feat: new explain-config and renamed explain-account commands * docs: adding documentation for new commands and options --- docs/cli-options.md | 14 ++- docs/cli-usage.md | 105 ++++++++++++++++- main.go | 2 + mkdocs.yml | 1 - pkg/awsutil/account.go | 14 +++ pkg/commands/account/account.go | 152 +++++++++++++++++++++++++ pkg/commands/config/config.go | 195 ++++++++++++++++++++++++++++++++ pkg/commands/nuke/command.go | 4 +- 8 files changed, 481 insertions(+), 6 deletions(-) create mode 100644 pkg/commands/account/account.go create mode 100644 pkg/commands/config/config.go diff --git a/docs/cli-options.md b/docs/cli-options.md index 673ce3cf..5ed861c0 100644 --- a/docs/cli-options.md +++ b/docs/cli-options.md @@ -2,10 +2,22 @@ This is not a comprehensive list of options, but rather a list of features that I think are worth highlighting. +## Cloud Control API + +`--cloud-control` will allow you to use the Cloud Control API for specific resource types. This is useful if you want to use the Cloud Control API for specific resource types. + ## Skip Alias Checks `--no-alias-check` will skip the check for the AWS account alias. This is useful if you are running in an account that does not have an alias. ## Skip Prompts -`--no-prompt` will skip the prompt to verify you want to run the command. This is useful if you are running in a CI/CD environment. \ No newline at end of file +`--no-prompt` will skip the prompt to verify you want to run the command. This is useful if you are running in a CI/CD environment. +`--prompt-delay` will set the delay before the command runs. This is useful if you want to give yourself time to cancel the command. + +## Logging + +- `--log-level` will set the log level. This is useful if you want to see more or less information in the logs. +- `--log-caller` will log the caller (aka line number and file). This is useful if you are debugging. +- `--log-disable-color` will disable log coloring. This is useful if you are running in an environment that does not support color. +- `--log-full-timestamp` will force log output to always show full timestamp. This is useful if you want to see the full timestamp in the logs. \ No newline at end of file diff --git a/docs/cli-usage.md b/docs/cli-usage.md index 68fbdfda..f9be0215 100644 --- a/docs/cli-usage.md +++ b/docs/cli-usage.md @@ -10,13 +10,15 @@ USAGE: aws-nuke [global options] command [command options] VERSION: - 3.0.0-beta.2 + 3.0.0-dev AUTHOR: Erik Kristensen COMMANDS: run, nuke run nuke against an aws account and remove everything from it + account-details, account list details about the AWS account that the tool is authenticated to + config-details explain the configuration file and the resources that will be nuked resource-types, list-resources list available resources to nuke help, h Shows a list of commands or help for one command @@ -50,4 +52,103 @@ OPTIONS: --log-disable-color disable log coloring (default: false) --log-full-timestamp force log output to always show full timestamp (default: false) --help, -h show help -``` \ No newline at end of file +``` + +## aws-nuke explain-account + +This command shows you details of how you are authenticated to AWS. + +```console +NAME: + aws-nuke explain-account - explain the account and authentication method used to authenticate against AWS + +USAGE: + aws-nuke explain-account [command options] [arguments...] + +DESCRIPTION: + explain the account and authentication method used to authenticate against AWS + +OPTIONS: + --config value, -c value path to config file (default: "config.yaml") + --default-region value the default aws region to use when setting up the aws auth session [$AWS_DEFAULT_REGION] + --access-key-id value the aws access key id to use when setting up the aws auth session [$AWS_ACCESS_KEY_ID] + --secret-access-key value the aws secret access key to use when setting up the aws auth session [$AWS_SECRET_ACCESS_KEY] + --session-token value the aws session token to use when setting up the aws auth session, typically used for temporary credentials [$AWS_SESSION_TOKEN] + --profile value the aws profile to use when setting up the aws auth session, typically used for shared credentials files [$AWS_PROFILE] + --assume-role-arn value the role arn to assume using the credentials provided in the profile or statically set [$AWS_ASSUME_ROLE_ARN] + --assume-role-session-name value the session name to provide for the assumed role [$AWS_ASSUME_ROLE_SESSION_NAME] + --assume-role-external-id value the external id to provide for the assumed role [$AWS_ASSUME_ROLE_EXTERNAL_ID] + --log-level value, -l value Log Level (default: "info") [$LOGLEVEL] + --log-caller log the caller (aka line number and file) (default: false) + --log-disable-color disable log coloring (default: false) + --log-full-timestamp force log output to always show full timestamp (default: false) + --help, -h show help +``` + +### explain-account example output + +```console +Overview: +> Account ID: 123456789012 +> Account ARN: arn:aws:iam::123456789012:root +> Account UserID: AKIAIOSFODNN7EXAMPLE:root +> Account Alias: no-alias-123456789012 +> Default Region: us-east-2 +> Enabled Regions: [global ap-south-1 ca-central-1 eu-central-1 us-west-1 us-west-2 eu-north-1 eu-west-3 eu-west-2 eu-west-1 ap-northeast-3 ap-northeast-2 ap-northeast-1 sa-east-1 ap-southeast-1 ap-southeast-2 us-east-1 us-east-2] + +Authentication: +> Method: Static Keys +> Access Key ID: AKIAIOSFODNN7EXAMPLE +``` + +## aws-nuke explain-config + +This command will explain the configuration file and the resources that will be nuked for the targeted account. + +```console +NAME: + aws-nuke explain-config - explain the configuration file and the resources that will be nuked for an account + +USAGE: + aws-nuke explain-config [command options] [arguments...] + +DESCRIPTION: + explain the configuration file and the resources that will be nuked for an account that + is defined within the configuration. You may either specific an account using the --account-id flag or + leave it empty to use the default account that can be authenticated against. If you want to see the + resource types that will be nuked, use the --with-resource-types flag. If you want to see the resources + that have filters defined, use the --with-resource-filters flag. + +OPTIONS: + --config value, -c value path to config file (default: "config.yaml") + --account-id value the account id to check against the configuration file, if empty, it will use whatever account + can be authenticated against + --with-resource-filters include resource with filters defined in the output (default: false) + --with-resource-types include resource types defined in the output (default: false) + --default-region value the default aws region to use when setting up the aws auth session [$AWS_DEFAULT_REGION] + --access-key-id value the aws access key id to use when setting up the aws auth session [$AWS_ACCESS_KEY_ID] + --secret-access-key value the aws secret access key to use when setting up the aws auth session [$AWS_SECRET_ACCESS_KEY] + --session-token value the aws session token to use when setting up the aws auth session, typically used for temporary credentials [$AWS_SESSION_TOKEN] + --profile value the aws profile to use when setting up the aws auth session, typically used for shared credentials files [$AWS_PROFILE] + --assume-role-arn value the role arn to assume using the credentials provided in the profile or statically set [$AWS_ASSUME_ROLE_ARN] + --assume-role-session-name value the session name to provide for the assumed role [$AWS_ASSUME_ROLE_SESSION_NAME] + --assume-role-external-id value the external id to provide for the assumed role [$AWS_ASSUME_ROLE_EXTERNAL_ID] + --log-level value, -l value Log Level (default: "info") [$LOGLEVEL] + --log-caller log the caller (aka line number and file) (default: false) + --log-disable-color disable log coloring (default: false) + --log-full-timestamp force log output to always show full timestamp (default: false) + --help, -h show help +``` + +### explain-config example output + +```console +Configuration Details + +Resource Types: 426 +Filter Presets: 2 +Resource Filters: 24 + +Note: use --with-resource-filters to see resources with filters defined +Note: use --with-resource-types to see included resource types that will be nuked +``` diff --git a/main.go b/main.go index 0c964014..77aef390 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,8 @@ import ( "github.com/ekristen/aws-nuke/pkg/common" + _ "github.com/ekristen/aws-nuke/pkg/commands/account" + _ "github.com/ekristen/aws-nuke/pkg/commands/config" _ "github.com/ekristen/aws-nuke/pkg/commands/list" _ "github.com/ekristen/aws-nuke/pkg/commands/nuke" diff --git a/mkdocs.yml b/mkdocs.yml index 3fbcef31..298ff333 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -76,7 +76,6 @@ nav: - Usage: cli-usage.md - Options: cli-options.md - Experimental: cli-experimental.md - - Feature Flags: cli-feature-flags.md - Examples: cli-examples.md - Config: - Overview: config.md diff --git a/pkg/awsutil/account.go b/pkg/awsutil/account.go index f1ee33c7..53e5da7c 100644 --- a/pkg/awsutil/account.go +++ b/pkg/awsutil/account.go @@ -19,6 +19,8 @@ type Account struct { Credentials id string + arn string + userID string aliases []string regions []string disabledRegions []string @@ -92,6 +94,8 @@ func NewAccount(creds Credentials, endpoints config.CustomEndpoints) (*Account, } account.id = ptr.ToString(identityOutput.Account) + account.arn = ptr.ToString(identityOutput.Arn) + account.userID = ptr.ToString(identityOutput.UserId) account.aliases = aliases account.regions = regions account.disabledRegions = disabledRegions @@ -104,6 +108,16 @@ func (a *Account) ID() string { return a.id } +// ARN returns the STS Authenticated ARN for the account +func (a *Account) ARN() string { + return a.arn +} + +// UserID returns the authenticated user ID +func (a *Account) UserID() string { + return a.userID +} + // Alias returns the first alias for the account func (a *Account) Alias() string { if len(a.aliases) == 0 { diff --git a/pkg/commands/account/account.go b/pkg/commands/account/account.go new file mode 100644 index 00000000..9757e4b0 --- /dev/null +++ b/pkg/commands/account/account.go @@ -0,0 +1,152 @@ +package account + +import ( + "fmt" + "github.com/aws/aws-sdk-go/aws/endpoints" + "github.com/ekristen/aws-nuke/pkg/awsutil" + "github.com/ekristen/aws-nuke/pkg/config" + libconfig "github.com/ekristen/libnuke/pkg/config" + "github.com/ekristen/libnuke/pkg/registry" + "github.com/sirupsen/logrus" + + "github.com/urfave/cli/v2" + + "github.com/ekristen/aws-nuke/pkg/commands/global" + "github.com/ekristen/aws-nuke/pkg/commands/nuke" + "github.com/ekristen/aws-nuke/pkg/common" +) + +func execute(c *cli.Context) error { + defaultRegion := c.String("default-region") + creds := nuke.ConfigureCreds(c) + + if err := creds.Validate(); err != nil { + return err + } + + // Parse the user supplied configuration file to pass in part to configure the nuke process. + parsedConfig, err := config.New(libconfig.Options{ + Path: c.Path("config"), + Deprecations: registry.GetDeprecatedResourceTypeMapping(), + }) + if err != nil { + logrus.Errorf("Failed to parse config file %s", c.Path("config")) + return err + } + + // Set the default region for the AWS SDK to use. + if defaultRegion != "" { + awsutil.DefaultRegionID = defaultRegion + switch defaultRegion { + case endpoints.UsEast1RegionID, endpoints.UsEast2RegionID, endpoints.UsWest1RegionID, endpoints.UsWest2RegionID: + awsutil.DefaultAWSPartitionID = endpoints.AwsPartitionID + case endpoints.UsGovEast1RegionID, endpoints.UsGovWest1RegionID: + awsutil.DefaultAWSPartitionID = endpoints.AwsUsGovPartitionID + default: + if parsedConfig.CustomEndpoints.GetRegion(defaultRegion) == nil { + err = fmt.Errorf("the custom region '%s' must be specified in the configuration 'endpoints'", defaultRegion) + logrus.Error(err.Error()) + return err + } + } + } + + // Create the AWS Account object. This will be used to get the account ID and aliases for the account. + account, err := awsutil.NewAccount(creds, parsedConfig.CustomEndpoints) + if err != nil { + return err + } + + fmt.Println("Overview:") + fmt.Println("> Account ID: ", account.ID()) + fmt.Println("> Account ARN: ", account.ARN()) + fmt.Println("> Account UserID: ", account.UserID()) + fmt.Println("> Account Alias: ", account.Alias()) + fmt.Println("> Default Region: ", defaultRegion) + fmt.Println("> Enabled Regions: ", account.Regions()) + + fmt.Println("") + fmt.Println("Authentication:") + if creds.HasKeys() { + fmt.Println("> Method: Static Keys") + fmt.Println("> Access Key ID: ", creds.AccessKeyID) + } + if creds.HasProfile() { + fmt.Println("> Method: Shared Credentials") + fmt.Println("> Profile: ", creds.Profile) + } + if creds.AssumeRoleArn != "" { + fmt.Println("> Method: Assume Role") + fmt.Println("> Role ARN: ", creds.AssumeRoleArn) + if creds.RoleSessionName != "" { + fmt.Println("> Session Name: ", creds.RoleSessionName) + } + if creds.ExternalId != "" { + fmt.Println("> External ID: ", creds.ExternalId) + } + } + + return nil +} + +func init() { + flags := []cli.Flag{ + &cli.PathFlag{ + Name: "config", + Aliases: []string{"c"}, + Usage: "path to config file", + Value: "config.yaml", + }, + &cli.StringFlag{ + Name: "default-region", + EnvVars: []string{"AWS_DEFAULT_REGION"}, + Usage: "the default aws region to use when setting up the aws auth session", + }, + &cli.StringFlag{ + Name: "access-key-id", + EnvVars: []string{"AWS_ACCESS_KEY_ID"}, + Usage: "the aws access key id to use when setting up the aws auth session", + }, + &cli.StringFlag{ + Name: "secret-access-key", + EnvVars: []string{"AWS_SECRET_ACCESS_KEY"}, + Usage: "the aws secret access key to use when setting up the aws auth session", + }, + &cli.StringFlag{ + Name: "session-token", + EnvVars: []string{"AWS_SESSION_TOKEN"}, + Usage: "the aws session token to use when setting up the aws auth session, typically used for temporary credentials", + }, + &cli.StringFlag{ + Name: "profile", + EnvVars: []string{"AWS_PROFILE"}, + Usage: "the aws profile to use when setting up the aws auth session, typically used for shared credentials files", + }, + &cli.StringFlag{ + Name: "assume-role-arn", + EnvVars: []string{"AWS_ASSUME_ROLE_ARN"}, + Usage: "the role arn to assume using the credentials provided in the profile or statically set", + }, + &cli.StringFlag{ + Name: "assume-role-session-name", + EnvVars: []string{"AWS_ASSUME_ROLE_SESSION_NAME"}, + Usage: "the session name to provide for the assumed role", + }, + &cli.StringFlag{ + Name: "assume-role-external-id", + EnvVars: []string{"AWS_ASSUME_ROLE_EXTERNAL_ID"}, + Usage: "the external id to provide for the assumed role", + }, + } + + cmd := &cli.Command{ + Name: "explain-account", + Usage: "explain the account and authentication method used to authenticate against AWS", + Description: `explain the account and authentication method used to authenticate against AWS`, + Flags: append(flags, global.Flags()...), + Before: global.Before, + Action: execute, + } + + common.RegisterCommand(cmd) +} diff --git a/pkg/commands/config/config.go b/pkg/commands/config/config.go new file mode 100644 index 00000000..80e22e0f --- /dev/null +++ b/pkg/commands/config/config.go @@ -0,0 +1,195 @@ +package config + +import ( + "fmt" + "github.com/ekristen/aws-nuke/pkg/awsutil" + "github.com/ekristen/aws-nuke/pkg/commands/global" + "github.com/ekristen/aws-nuke/pkg/commands/nuke" + "github.com/ekristen/aws-nuke/pkg/common" + "github.com/ekristen/aws-nuke/pkg/config" + libconfig "github.com/ekristen/libnuke/pkg/config" + "github.com/ekristen/libnuke/pkg/registry" + "github.com/ekristen/libnuke/pkg/types" + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" + "slices" +) + +func execute(c *cli.Context) error { + accountID := c.String("account-id") + + parsedConfig, err := config.New(libconfig.Options{ + Path: c.Path("config"), + Deprecations: registry.GetDeprecatedResourceTypeMapping(), + }) + if err != nil { + logrus.Errorf("Failed to parse config file %s", c.Path("config")) + return err + } + + if accountID != "" { + creds := nuke.ConfigureCreds(c) + if err := creds.Validate(); err != nil { + return err + } + + // Create the AWS Account object. This will be used to get the account ID and aliases for the account. + account, err := awsutil.NewAccount(creds, parsedConfig.CustomEndpoints) + if err != nil { + return err + } + + accountID = account.ID() + } + + // Get any specific account level configuration + accountConfig := parsedConfig.Accounts[accountID] + + if accountConfig == nil { + return fmt.Errorf("account is not configured in the config file") + } + + // Resolve the resource types to be used for the nuke process based on the parameters, global configuration, and + // account level configuration. + resourceTypes := types.ResolveResourceTypes( + registry.GetNames(), + []types.Collection{ + types.Collection{}, // note: empty collection since we are not capturing parameters + parsedConfig.ResourceTypes.GetIncludes(), + accountConfig.ResourceTypes.GetIncludes(), + }, + []types.Collection{ + types.Collection{}, // note: empty collection since we are not capturing parameters + parsedConfig.ResourceTypes.Excludes, + accountConfig.ResourceTypes.Excludes, + }, + []types.Collection{ + types.Collection{}, // note: empty collection since we are not capturing parameters + parsedConfig.ResourceTypes.GetAlternatives(), + accountConfig.ResourceTypes.GetAlternatives(), + }, + registry.GetAlternativeResourceTypeMapping(), + ) + + filtersTotal := 0 + var resourcesWithFilters []string + for name, preset := range parsedConfig.Presets { + if !slices.Contains(accountConfig.Presets, name) { + continue + } + + filtersTotal += len(preset.Filters) + + for resource := range preset.Filters { + resourcesWithFilters = append(resourcesWithFilters, resource) + } + } + + fmt.Printf("Configuration Details\n\n") + + fmt.Printf("Resource Types: %d\n", len(resourceTypes)) + fmt.Printf("Filter Presets: %d\n", len(accountConfig.Presets)) + fmt.Printf("Resource Filters: %d\n", filtersTotal) + + fmt.Println("") + + if c.Bool("with-resource-filters") { + fmt.Println("Resources with Filters Defined:") + for _, resource := range resourcesWithFilters { + fmt.Printf(" %s\n", resource) + } + fmt.Println("") + } else { + fmt.Printf("Note: use --with-resource-filters to see resources with filters defined\n") + } + + if c.Bool("with-resource-types") { + fmt.Println("Resource Types:") + for _, resourceType := range resourceTypes { + fmt.Printf(" %s\n", resourceType) + } + fmt.Println("") + } else { + fmt.Printf("Note: use --with-resource-types to see included resource types that will be nuked\n") + } + + return nil +} + +func init() { + flags := []cli.Flag{ + &cli.PathFlag{ + Name: "config", + Aliases: []string{"c"}, + Usage: "path to config file", + Value: "config.yaml", + }, + &cli.StringFlag{ + Name: "account-id", + Usage: `the account id to check against the configuration file, if empty, it will use whatever account can be authenticated against`, + }, + &cli.BoolFlag{ + Name: "with-resource-filters", + Usage: "include resource with filters defined in the output", + }, + &cli.BoolFlag{ + Name: "with-resource-types", + Usage: "include resource types defined in the output", + }, + &cli.StringFlag{ + Name: "default-region", + EnvVars: []string{"AWS_DEFAULT_REGION"}, + Usage: "the default aws region to use when setting up the aws auth session", + }, + &cli.StringFlag{ + Name: "access-key-id", + EnvVars: []string{"AWS_ACCESS_KEY_ID"}, + Usage: "the aws access key id to use when setting up the aws auth session", + }, + &cli.StringFlag{ + Name: "secret-access-key", + EnvVars: []string{"AWS_SECRET_ACCESS_KEY"}, + Usage: "the aws secret access key to use when setting up the aws auth session", + }, + &cli.StringFlag{ + Name: "session-token", + EnvVars: []string{"AWS_SESSION_TOKEN"}, + Usage: "the aws session token to use when setting up the aws auth session, typically used for temporary credentials", + }, + &cli.StringFlag{ + Name: "profile", + EnvVars: []string{"AWS_PROFILE"}, + Usage: "the aws profile to use when setting up the aws auth session, typically used for shared credentials files", + }, + &cli.StringFlag{ + Name: "assume-role-arn", + EnvVars: []string{"AWS_ASSUME_ROLE_ARN"}, + Usage: "the role arn to assume using the credentials provided in the profile or statically set", + }, + &cli.StringFlag{ + Name: "assume-role-session-name", + EnvVars: []string{"AWS_ASSUME_ROLE_SESSION_NAME"}, + Usage: "the session name to provide for the assumed role", + }, + &cli.StringFlag{ + Name: "assume-role-external-id", + EnvVars: []string{"AWS_ASSUME_ROLE_EXTERNAL_ID"}, + Usage: "the external id to provide for the assumed role", + }, + } + + cmd := &cli.Command{ + Name: "explain-config", + Usage: "explain the configuration file and the resources that will be nuked for an account", + Description: `explain the configuration file and the resources that will be nuked for an account that +is defined within the configuration. You may either specific an account using the --account-id flag or +leave it empty to use the default account that can be authenticated against. If you want to see the +resource types that will be nuked, use the --with-resource-types flag. If you want to see the resources +that have filters defined, use the --with-resource-filters flag.`, + Flags: append(flags, global.Flags()...), + Before: global.Before, + Action: execute, + } + + common.RegisterCommand(cmd) +} diff --git a/pkg/commands/nuke/command.go b/pkg/commands/nuke/command.go index fc38fae0..41b68ffc 100644 --- a/pkg/commands/nuke/command.go +++ b/pkg/commands/nuke/command.go @@ -25,7 +25,7 @@ import ( "github.com/ekristen/aws-nuke/pkg/nuke" ) -func configureCreds(c *cli.Context) (creds awsutil.Credentials) { +func ConfigureCreds(c *cli.Context) (creds awsutil.Credentials) { creds.Profile = c.String("profile") creds.AccessKeyID = c.String("access-key-id") creds.SecretAccessKey = c.String("secret-access-key") @@ -42,7 +42,7 @@ func execute(c *cli.Context) error { defer cancel() defaultRegion := c.String("default-region") - creds := configureCreds(c) + creds := ConfigureCreds(c) if err := creds.Validate(); err != nil { return err