diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..64bd5cee --- /dev/null +++ b/.editorconfig @@ -0,0 +1,132 @@ +# Distributed via https://github.com/rebuy-de/terraform-cluster-config +# Modify only there, changes in project repos will be overwritten + +root = true + +[openapi-spec.yaml] +ij_formatter_enabled = false + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +max_line_length = 120 +tab_width = 4 +trim_trailing_whitespace = true +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = true +ij_smart_tabs = false +ij_visual_guides = +ij_wrap_on_typing = false + +[{*.htm,*.html,*.sht,*.shtm,*.shtml}] +ij_html_attribute_wrap = off +ij_html_do_not_indent_children_of_tags = +ij_html_keep_blank_lines = 1 +ij_html_text_wrap = off + +[{*.cjs,*.js}] +ij_javascript_do_while_brace_force = always +ij_javascript_for_brace_force = always +ij_javascript_if_brace_force = always +ij_javascript_keep_blank_lines_in_code = 1 +ij_javascript_use_double_quotes = false +ij_javascript_while_brace_force = always + +[{*.ats,*.cts,*.mts,*.ts}] +ij_typescript_do_while_brace_force = always +ij_typescript_for_brace_force = always +ij_typescript_if_brace_force = always +ij_typescript_import_prefer_absolute_path = true +ij_typescript_keep_blank_lines_in_code = 1 +ij_typescript_space_before_function_left_parenth = false +ij_typescript_use_double_quotes = false +ij_typescript_while_brace_force = always + +[*.coffee] +indent_size = 2 + +[*.java] +ij_continuation_indent_size = 4 +ij_java_blank_lines_around_field = 1 +ij_java_blank_lines_around_initializer = 0 +ij_java_class_brace_style = next_line +ij_java_class_count_to_use_import_on_demand = 99 +ij_java_do_while_brace_force = always +ij_java_doc_add_blank_line_after_param_comments = true +ij_java_doc_add_blank_line_after_return = true +ij_java_for_brace_force = always +ij_java_if_brace_force = always +ij_java_keep_blank_lines_before_right_brace = 0 +ij_java_keep_blank_lines_in_code = 1 +ij_java_keep_blank_lines_in_declarations = 0 +ij_java_keep_simple_classes_in_one_line = true +ij_java_keep_simple_lambdas_in_one_line = true +ij_java_method_brace_style = next_line +ij_java_names_count_to_use_import_on_demand = 99 +ij_java_new_line_after_lparen_in_record_header = true +ij_java_packages_to_use_import_on_demand = +ij_java_record_components_wrap = on_every_item +ij_java_rparen_on_new_line_in_record_header = true +ij_java_while_brace_force = always + +[{*.kt,*.kts}] +ij_continuation_indent_size = 4 +ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL +ij_kotlin_name_count_to_use_star_import = 2147483647 +ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 +ij_kotlin_packages_to_use_import_on_demand = + +[{*.ctp,*.hphp,*.inc,*.module,*.php,*.php4,*.php5,*.phtml}] +ij_php_align_multiline_parameters = false +ij_php_blank_lines_around_field = 1 +ij_php_blank_lines_before_return_statement = 1 +ij_php_comma_after_last_array_element = true +ij_php_force_short_declaration_array_style = true +ij_php_keep_blank_lines_before_right_brace = 0 +ij_php_keep_blank_lines_in_code = 1 +ij_php_keep_blank_lines_in_declarations = 0 +ij_php_keep_rparen_and_lbrace_on_one_line = true +ij_php_lower_case_boolean_const = true +ij_php_lower_case_null_const = true +ij_php_method_parameters_new_line_after_left_paren = true +ij_php_method_parameters_right_paren_on_new_line = true +ij_php_phpdoc_blank_line_before_tags = true +ij_php_phpdoc_blank_lines_around_parameters = true +ij_php_space_after_type_cast = true +ij_php_space_before_short_closure_left_parenthesis = true + +[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.pom,*.rng,*.tld,*.wadl,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul,phpunit.xml.dist}] +ij_xml_space_inside_empty_tag = true + +[{*.tf,*.tfvars,*.hcl}] +tab_width = 2 +ij_continuation_indent_size = 4 + +[*.less] +tab_width = 2 +ij_continuation_indent_size = 2 + +[*.sass] +tab_width = 2 +ij_continuation_indent_size = 2 + +[*.scala] +ij_scala_do_while_brace_force = always +ij_scala_for_brace_force = always +ij_scala_if_brace_force = always +ij_scala_keep_blank_lines_before_right_brace = 0 +ij_scala_keep_blank_lines_in_code = 0 +ij_scala_keep_blank_lines_in_declarations = 0 +ij_scala_multiline_string_closing_quotes_on_new_line = false + +[*.scss] +ij_continuation_indent_size = 4 + +[{*.yaml,*.yml}] +ij_yaml_spaces_within_braces = false +ij_yaml_spaces_within_brackets = false diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..91d476ed --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" + day: "tuesday" + time: "10:00" + timezone: "Europe/Berlin" + groups: + golang: + patterns: + - "*" diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 00000000..4d213e6f --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,13 @@ +# https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes#configuring-automatically-generated-release-notes + +changelog: + categories: + - title: Notable changes + labels: + - '*' + exclude: + labels: + - dependencies + - title: Dependency updates + labels: + - dependencies diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000..980065e3 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,38 @@ +name: Golang CI + +on: + push: + branches: [oreilly-main] + pull_request: + types: [opened, reopened, synchronize] + schedule: + - cron: '15 3 * * 0' + workflow_dispatch: + +jobs: + build: + name: CI Build + runs-on: ubuntu-22.04 + steps: + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.22' + - name: Setup tools + run: | + go install golang.org/x/lint/golint@latest + - name: Checkout code + uses: actions/checkout@v4 + - name: Check Formatting + run: | + if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then + echo "### Go formatting is off, please execute 'gofmt -w -s .' - see following diff: ###" + gofmt -s -d . + exit 1 + fi + - name: Test Project + run: | + make test + - name: Build Project + run: | + make \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 00000000..02e0088f --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,55 @@ +name: Publish release artifacts + +on: + release: + types: [created] + +jobs: + update_readme: + name: Update Readme + runs-on: ubuntu-22.04 + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: main + - name: Update versions in readme + run: | + sed -r -i "s/aws-nuke:v[0-9]+\.[0-9]+\.[0-9]+/aws-nuke:${{ github.ref_name }}/" README.md + sed -r -i "s/aws-nuke-v[0-9]+\.[0-9]+\.[0-9]+/aws-nuke-${{ github.ref_name }}/" README.md + sed -r -i "s/\/v[0-9]+\.[0-9]+\.[0-9]+\//\/${{ github.ref_name }}\//" README.md + - uses: peter-evans/create-pull-request@v6 + name: Create Pull Request + with: + title: Update readme for ${{ github.ref_name }} release + commit-message: Update readme for ${{ github.ref_name }} release + body: Updating version references in the readme to ${{ github.ref_name }} + branch: update-readme-${{ github.ref_name }} + delete-branch: true + + release: + name: Publish binaries + runs-on: ubuntu-22.04 + steps: + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.22' + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Build Project binaries + env: + CGO_ENABLED: 0 + run: | + make xc + - name: Upload binaries to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: dist/aws* + tag: ${{ github.ref }} + overwrite: true + file_glob: true \ No newline at end of file diff --git a/cmd/nuke.go b/cmd/nuke.go new file mode 100644 index 00000000..7a184892 --- /dev/null +++ b/cmd/nuke.go @@ -0,0 +1,333 @@ +package cmd + +import ( + "fmt" + "time" + + "github.com/rebuy-de/aws-nuke/v2/pkg/awsutil" + "github.com/rebuy-de/aws-nuke/v2/pkg/config" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" + "github.com/rebuy-de/aws-nuke/v2/resources" + "github.com/sirupsen/logrus" +) + +type Nuke struct { + Parameters NukeParameters + Account awsutil.Account + Config *config.Nuke + + ResourceTypes types.Collection + + items Queue +} + +func NewNuke(params NukeParameters, account awsutil.Account) *Nuke { + n := Nuke{ + Parameters: params, + Account: account, + } + + return &n +} + +func (n *Nuke) Run() error { + var err error + + if n.Parameters.ForceSleep < 3 && n.Parameters.NoDryRun { + return fmt.Errorf("Value for --force-sleep cannot be less than 3 seconds if --no-dry-run is set. This is for your own protection.") + } + forceSleep := time.Duration(n.Parameters.ForceSleep) * time.Second + + fmt.Printf("aws-nuke version %s - %s - %s\n\n", BuildVersion, BuildDate, BuildHash) + + err = n.Config.ValidateAccount(n.Account.ID(), n.Account.Aliases()) + if err != nil { + return err + } + + fmt.Printf("Do you really want to nuke the account with "+ + "the ID %s and the alias '%s'?\n", n.Account.ID(), n.Account.Alias()) + if n.Parameters.Force { + fmt.Printf("Waiting %v before continuing.\n", forceSleep) + time.Sleep(forceSleep) + } else { + fmt.Printf("Do you want to continue? Enter account alias to continue.\n") + err = Prompt(n.Account.Alias()) + if err != nil { + return err + } + } + + err = n.Scan() + if err != nil { + return err + } + + if n.items.Count(ItemStateFailed) > 0 && n.items.Count(ItemStateNew) == 0 { + for _, item := range n.items { + if item.State != ItemStateFailed { + continue + } + logrus.Error(fmt.Sprintf("%s. %s.", item.Type, item.Reason)) + } + return fmt.Errorf("failed") + } + + if n.items.Count(ItemStateNew) == 0 { + fmt.Println("No resource to delete.") + return nil + } + + if !n.Parameters.NoDryRun { + fmt.Println("The above resources would be deleted with the supplied configuration. Provide --no-dry-run to actually destroy resources.") + return nil + } + + fmt.Printf("Do you really want to nuke these resources on the account with "+ + "the ID %s and the alias '%s'?\n", n.Account.ID(), n.Account.Alias()) + if n.Parameters.Force { + fmt.Printf("Waiting %v before continuing.\n", forceSleep) + time.Sleep(forceSleep) + } else { + fmt.Printf("Do you want to continue? Enter account alias to continue.\n") + err = Prompt(n.Account.Alias()) + if err != nil { + return err + } + } + + failCount := 0 + waitingCount := 0 + + for { + n.HandleQueue() + + if n.items.Count(ItemStatePending, ItemStateWaiting, ItemStateNew) == 0 && n.items.Count(ItemStateFailed) > 0 { + if failCount >= 2 { + logrus.Errorf("There are resources in failed state, but none are ready for deletion, anymore.") + fmt.Println() + + for _, item := range n.items { + if item.State != ItemStateFailed { + continue + } + + item.Print() + logrus.Error(item.Reason) + } + + return fmt.Errorf("failed") + } + + failCount = failCount + 1 + } else { + failCount = 0 + } + if n.Parameters.MaxWaitRetries != 0 && n.items.Count(ItemStateWaiting, ItemStatePending) > 0 && n.items.Count(ItemStateNew) == 0 { + if waitingCount >= n.Parameters.MaxWaitRetries { + return fmt.Errorf("Max wait retries of %d exceeded.\n\n", n.Parameters.MaxWaitRetries) + } + waitingCount = waitingCount + 1 + } else { + waitingCount = 0 + } + if n.items.Count(ItemStateNew, ItemStatePending, ItemStateFailed, ItemStateWaiting) == 0 { + break + } + + time.Sleep(5 * time.Second) + } + + fmt.Printf("Nuke complete: %d failed, %d skipped, %d finished.\n\n", + n.items.Count(ItemStateFailed), n.items.Count(ItemStateFiltered), n.items.Count(ItemStateFinished)) + + return nil +} + +func (n *Nuke) Scan() error { + accountConfig := n.Config.Accounts[n.Account.ID()] + + resourceTypes := ResolveResourceTypes( + resources.GetListerNames(), + resources.GetCloudControlMapping(), + []types.Collection{ + n.Parameters.Targets, + n.Config.ResourceTypes.Targets, + accountConfig.ResourceTypes.Targets, + }, + []types.Collection{ + n.Parameters.Excludes, + n.Config.ResourceTypes.Excludes, + accountConfig.ResourceTypes.Excludes, + }, + []types.Collection{ + n.Parameters.CloudControl, + n.Config.ResourceTypes.CloudControl, + accountConfig.ResourceTypes.CloudControl, + }, + ) + + queue := make(Queue, 0) + + for _, regionName := range n.Config.Regions { + region := NewRegion(regionName, n.Account.ResourceTypeToServiceType, n.Account.NewSession) + + items := Scan(region, resourceTypes) + for item := range items { + ffGetter, ok := item.Resource.(resources.FeatureFlagGetter) + if ok { + ffGetter.FeatureFlags(n.Config.FeatureFlags) + } + + queue = append(queue, item) + err := n.Filter(item) + if err != nil { + return err + } + + if item.State != ItemStateFiltered || !n.Parameters.Quiet { + item.Print() + } + } + } + + fmt.Printf("Scan complete: %d total, %d nukeable, %d filtered.\n\n", + queue.CountTotal(), queue.Count(ItemStateNew), queue.Count(ItemStateFiltered)) + + n.items = queue + + return nil +} + +func (n *Nuke) Filter(item *Item) error { + + checker, ok := item.Resource.(resources.Filter) + if ok { + err := checker.Filter() + if err != nil { + item.State = ItemStateFiltered + item.Reason = err.Error() + + // Not returning the error, since it could be because of a failed + // request to the API. We do not want to block the whole nuking, + // because of an issue on AWS side. + return nil + } + } + + accountFilters, err := n.Config.Filters(n.Account.ID()) + if err != nil { + return err + } + + itemFilters, ok := accountFilters[item.Type] + if !ok { + return nil + } + + for _, filter := range itemFilters { + prop, err := item.GetProperty(filter.Property) + if err != nil { + logrus.Warnf(err.Error()) + continue + } + match, err := filter.Match(prop) + if err != nil { + return err + } + + if IsTrue(filter.Invert) { + match = !match + } + + if match { + item.State = ItemStateFiltered + item.Reason = "filtered by config" + return nil + } + } + + return nil +} + +func (n *Nuke) HandleQueue() { + listCache := make(map[string]map[string][]resources.Resource) + + for _, item := range n.items { + switch item.State { + case ItemStateNew: + n.HandleRemove(item) + item.Print() + case ItemStateFailed: + // item.Resource will be nil if an exception was thrown while retrieving cloudControl + // resourceType's items (I.E resourceTypes lister()), however we still pass down the + // reason and state so we aren't ignoring these exceptions. + if item.Resource != nil { + n.HandleRemove(item) + n.HandleWait(item, listCache) + item.Print() + } + case ItemStatePending: + n.HandleWait(item, listCache) + item.State = ItemStateWaiting + item.Print() + case ItemStateWaiting: + n.HandleWait(item, listCache) + item.Print() + } + + } + + fmt.Println() + fmt.Printf("Removal requested: %d waiting, %d failed, %d skipped, %d finished\n\n", + n.items.Count(ItemStateWaiting, ItemStatePending), n.items.Count(ItemStateFailed), + n.items.Count(ItemStateFiltered), n.items.Count(ItemStateFinished)) +} + +func (n *Nuke) HandleRemove(item *Item) { + err := item.Resource.Remove() + if err != nil { + item.State = ItemStateFailed + item.Reason = err.Error() + return + } + + item.State = ItemStatePending + item.Reason = "" +} + +func (n *Nuke) HandleWait(item *Item, cache map[string]map[string][]resources.Resource) { + var err error + region := item.Region.Name + _, ok := cache[region] + if !ok { + cache[region] = map[string][]resources.Resource{} + } + left, ok := cache[region][item.Type] + if !ok { + left, err = item.List() + if err != nil { + item.State = ItemStateFailed + item.Reason = err.Error() + return + } + cache[region][item.Type] = left + } + + for _, r := range left { + if item.Equals(r) { + checker, ok := r.(resources.Filter) + if ok { + err := checker.Filter() + if err != nil { + break + } + } + + return + } + } + + item.State = ItemStateFinished + item.Reason = "" +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 00000000..c39b1949 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,186 @@ +package cmd + +import ( + "fmt" + "os" + "sort" + + "github.com/aws/aws-sdk-go/aws/endpoints" + "github.com/rebuy-de/aws-nuke/v2/pkg/awsutil" + "github.com/rebuy-de/aws-nuke/v2/pkg/config" + "github.com/rebuy-de/aws-nuke/v2/resources" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func NewRootCommand() *cobra.Command { + var ( + params NukeParameters + creds awsutil.Credentials + defaultRegion string + verbose bool + ) + + command := &cobra.Command{ + Use: "aws-nuke", + Short: "aws-nuke removes every resource from AWS", + Long: `A tool which removes every resource from an AWS account. Use it with caution, since it cannot distinguish between production and non-production.`, + } + + command.PreRun = func(cmd *cobra.Command, args []string) { + log.SetLevel(log.InfoLevel) + if verbose { + log.SetLevel(log.DebugLevel) + } + log.SetFormatter(&log.TextFormatter{ + EnvironmentOverrideColors: true, + }) + } + + command.RunE = func(cmd *cobra.Command, args []string) error { + var err error + + err = params.Validate() + if err != nil { + return err + } + + if !creds.HasKeys() && !creds.HasProfile() && defaultRegion != "" { + creds.AccessKeyID = os.Getenv("AWS_ACCESS_KEY_ID") + creds.SecretAccessKey = os.Getenv("AWS_SECRET_ACCESS_KEY") + } + err = creds.Validate() + if err != nil { + return err + } + + command.SilenceUsage = true + + config, err := config.Load(params.ConfigPath) + if err != nil { + log.Errorf("Failed to parse config file %s", params.ConfigPath) + return err + } + + 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 + case endpoints.CnNorth1RegionID, endpoints.CnNorthwest1RegionID: + awsutil.DefaultAWSPartitionID = endpoints.AwsCnPartitionID + default: + if config.CustomEndpoints.GetRegion(defaultRegion) == nil { + err = fmt.Errorf("The custom region '%s' must be specified in the configuration 'endpoints'", defaultRegion) + log.Error(err.Error()) + return err + } + } + } + + account, err := awsutil.NewAccount(creds, config.CustomEndpoints) + if err != nil { + return err + } + + n := NewNuke(params, *account) + + n.Config = config + + return n.Run() + } + + command.PersistentFlags().BoolVarP( + &verbose, "verbose", "v", false, + "Enables debug output.") + + command.PersistentFlags().StringVarP( + ¶ms.ConfigPath, "config", "c", "", + "(required) Path to the nuke config file.") + + command.PersistentFlags().StringVar( + &creds.Profile, "profile", "", + "Name of the AWS profile name for accessing the AWS API. "+ + "Cannot be used together with --access-key-id and --secret-access-key.") + command.PersistentFlags().StringVar( + &creds.AccessKeyID, "access-key-id", "", + "AWS access key ID for accessing the AWS API. "+ + "Must be used together with --secret-access-key. "+ + "Cannot be used together with --profile.") + command.PersistentFlags().StringVar( + &creds.SecretAccessKey, "secret-access-key", "", + "AWS secret access key for accessing the AWS API. "+ + "Must be used together with --access-key-id. "+ + "Cannot be used together with --profile.") + command.PersistentFlags().StringVar( + &creds.SessionToken, "session-token", "", + "AWS session token for accessing the AWS API. "+ + "Must be used together with --access-key-id and --secret-access-key. "+ + "Cannot be used together with --profile.") + command.PersistentFlags().StringVar( + &creds.AssumeRoleArn, "assume-role-arn", "", + "AWS IAM role arn to assume. "+ + "The credentials provided via --access-key-id or --profile must "+ + "be allowed to assume this role. ") + command.PersistentFlags().StringVar( + &defaultRegion, "default-region", "", + "Custom default region name.") + + command.PersistentFlags().StringSliceVarP( + ¶ms.Targets, "target", "t", []string{}, + "Limit nuking to certain resource types (eg IAMServerCertificate). "+ + "This flag can be used multiple times.") + command.PersistentFlags().StringSliceVarP( + ¶ms.Excludes, "exclude", "e", []string{}, + "Prevent nuking of certain resource types (eg IAMServerCertificate). "+ + "This flag can be used multiple times.") + command.PersistentFlags().StringSliceVar( + ¶ms.CloudControl, "cloud-control", []string{}, + "Nuke given resource via Cloud Control API. "+ + "If there is an old-style method for the same resource, the old-style one will not be executed. "+ + "Note that old-style and cloud-control filters are not compatible! "+ + "This flag can be used multiple times.") + command.PersistentFlags().BoolVar( + ¶ms.NoDryRun, "no-dry-run", false, + "If specified, it actually deletes found resources. "+ + "Otherwise it just lists all candidates.") + command.PersistentFlags().BoolVar( + ¶ms.Force, "force", false, + "Don't ask for confirmation before deleting resources. "+ + "Instead it waits 15s before continuing. Set --force-sleep to change the wait time.") + command.PersistentFlags().IntVar( + ¶ms.ForceSleep, "force-sleep", 15, + "If specified and --force is set, wait this many seconds before deleting resources. "+ + "Defaults to 15.") + command.PersistentFlags().IntVar( + ¶ms.MaxWaitRetries, "max-wait-retries", 0, + "If specified, the program will exit if resources are stuck in waiting for this many iterations. "+ + "0 (default) disables early exit.") + command.PersistentFlags().BoolVarP( + ¶ms.Quiet, "quiet", "q", false, + "Don't show filtered resources.") + + command.AddCommand(NewVersionCommand()) + command.AddCommand(NewResourceTypesCommand()) + + return command +} + +func NewResourceTypesCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "resource-types", + Short: "lists all available resource types", + Run: func(cmd *cobra.Command, args []string) { + names := resources.GetListerNames() + sort.Strings(names) + + for _, resourceType := range names { + fmt.Println(resourceType) + } + }, + } + + return cmd +} diff --git a/cmd/scan.go b/cmd/scan.go new file mode 100644 index 00000000..815816ca --- /dev/null +++ b/cmd/scan.go @@ -0,0 +1,103 @@ +package cmd + +import ( + "context" + "fmt" + "runtime/debug" + + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/rebuy-de/aws-nuke/v2/pkg/awsutil" + "github.com/rebuy-de/aws-nuke/v2/pkg/util" + "github.com/rebuy-de/aws-nuke/v2/resources" + log "github.com/sirupsen/logrus" + "golang.org/x/sync/semaphore" +) + +const ScannerParallelQueries = 16 + +func Scan(region *Region, resourceTypes []string) <-chan *Item { + s := &scanner{ + items: make(chan *Item, 100), + semaphore: semaphore.NewWeighted(ScannerParallelQueries), + } + go s.run(region, resourceTypes) + + return s.items +} + +type scanner struct { + items chan *Item + semaphore *semaphore.Weighted +} + +func (s *scanner) run(region *Region, resourceTypes []string) { + ctx := context.Background() + + for _, resourceType := range resourceTypes { + s.semaphore.Acquire(ctx, 1) + go s.list(region, resourceType) + } + + // Wait for all routines to finish. + s.semaphore.Acquire(ctx, ScannerParallelQueries) + + close(s.items) +} + +func (s *scanner) list(region *Region, resourceType string) { + defer func() { + if r := recover(); r != nil { + err := fmt.Errorf("%v\n\n%s", r.(error), string(debug.Stack())) + dump := util.Indent(fmt.Sprintf("%v", err), " ") + log.Errorf("Listing %s failed:\n%s", resourceType, dump) + } + }() + defer s.semaphore.Release(1) + + lister := resources.GetLister(resourceType) + var rs []resources.Resource + sess, err := region.Session(resourceType) + if err == nil { + rs, err = lister(sess) + } + if err != nil { + _, ok := err.(awsutil.ErrSkipRequest) + if ok { + log.Debugf("skipping request: %v", err) + return + } + + _, ok = err.(awsutil.ErrUnknownEndpoint) + if ok { + log.Warnf("skipping request: %v", err) + return + } + + awsErr, ok := err.(awserr.Error) + if ok && awsErr.Code() == "ThrottlingException" { + s.items <- &Item{ + Region: region, + Resource: nil, + State: ItemStateFailed, + Reason: err.Error(), + Type: resourceType, + } + dump := util.Indent(fmt.Sprintf("%v", err), " ") + log.Errorf("Listing %s failed:\n%s", resourceType, dump) + return + } + + dump := util.Indent(fmt.Sprintf("%v", err), " ") + log.Errorf("Listing %s failed:\n%s", resourceType, dump) + return + } + + for _, r := range rs { + s.items <- &Item{ + Region: region, + Resource: r, + State: ItemStateNew, + Type: resourceType, + } + } +} diff --git a/pkg/config/filter.go b/pkg/config/filter.go new file mode 100644 index 00000000..019ed8eb --- /dev/null +++ b/pkg/config/filter.go @@ -0,0 +1,131 @@ +package config + +import ( + "fmt" + "regexp" + "strconv" + "strings" + "time" + + "github.com/mb0/glob" +) + +type FilterType string + +const ( + FilterTypeEmpty FilterType = "" + FilterTypeExact = "exact" + FilterTypeGlob = "glob" + FilterTypeRegex = "regex" + FilterTypeContains = "contains" + FilterTypeDateOlderThan = "dateOlderThan" +) + +type Filters map[string][]Filter + +func (f Filters) Merge(f2 Filters) { + for resourceType, filter := range f2 { + f[resourceType] = append(f[resourceType], filter...) + } +} + +type Filter struct { + Property string + Type FilterType + Value string + Invert string +} + +func (f Filter) Match(o string) (bool, error) { + switch f.Type { + case FilterTypeEmpty: + fallthrough + + case FilterTypeExact: + return f.Value == o, nil + + case FilterTypeContains: + return strings.Contains(o, f.Value), nil + + case FilterTypeGlob: + return glob.Match(f.Value, o) + + case FilterTypeRegex: + re, err := regexp.Compile(f.Value) + if err != nil { + return false, err + } + return re.MatchString(o), nil + + case FilterTypeDateOlderThan: + if o == "" { + return false, nil + } + duration, err := time.ParseDuration(f.Value) + if err != nil { + return false, err + } + fieldTime, err := parseDate(o) + if err != nil { + return false, err + } + fieldTimeWithOffset := fieldTime.Add(duration) + + return fieldTimeWithOffset.After(time.Now()), nil + + default: + return false, fmt.Errorf("unknown type %s", f.Type) + } +} + +func parseDate(input string) (time.Time, error) { + if i, err := strconv.ParseInt(input, 10, 64); err == nil { + t := time.Unix(i, 0) + return t, nil + } + + formats := []string{ + "2006-01-02", + "2006/01/02", + "2006-01-02T15:04:05Z", + "2006-01-02 15:04:05 -0700 MST", // Date format used by AWS for CreateTime on ASGs + time.RFC3339Nano, // Format of t.MarshalText() and t.MarshalJSON() + time.RFC3339, + } + for _, f := range formats { + t, err := time.Parse(f, input) + if err == nil { + return t, nil + } + } + return time.Now(), fmt.Errorf("unable to parse time %s", input) +} + +func (f *Filter) UnmarshalYAML(unmarshal func(interface{}) error) error { + var value string + + if unmarshal(&value) == nil { + f.Type = FilterTypeExact + f.Value = value + return nil + } + + m := map[string]string{} + err := unmarshal(m) + if err != nil { + return err + } + + f.Type = FilterType(m["type"]) + f.Value = m["value"] + f.Property = m["property"] + f.Invert = m["invert"] + return nil +} + +func NewExactFilter(value string) Filter { + return Filter{ + Type: FilterTypeExact, + Value: value, + } +} diff --git a/pkg/config/filter_test.go b/pkg/config/filter_test.go new file mode 100644 index 00000000..25f0d236 --- /dev/null +++ b/pkg/config/filter_test.go @@ -0,0 +1,105 @@ +package config_test + +import ( + "strconv" + "testing" + "time" + + "github.com/rebuy-de/aws-nuke/v2/pkg/config" + yaml "gopkg.in/yaml.v3" +) + +func TestUnmarshalFilter(t *testing.T) { + past := time.Now().UTC().Add(-24 * time.Hour) + future := time.Now().UTC().Add(24 * time.Hour) + cases := []struct { + yaml string + match, mismatch []string + }{ + { + yaml: `foo`, + match: []string{"foo"}, + mismatch: []string{"fo", "fooo", "o", "fo"}, + }, + { + yaml: `{"type":"exact","value":"foo"}`, + match: []string{"foo"}, + mismatch: []string{"fo", "fooo", "o", "fo"}, + }, + { + yaml: `{"type":"glob","value":"b*sh"}`, + match: []string{"bish", "bash", "bosh", "bush", "boooooosh", "bsh"}, + mismatch: []string{"woooosh", "fooo", "o", "fo"}, + }, + { + yaml: `{"type":"glob","value":"b?sh"}`, + match: []string{"bish", "bash", "bosh", "bush"}, + mismatch: []string{"woooosh", "fooo", "o", "fo", "boooooosh", "bsh"}, + }, + { + yaml: `{"type":"regex","value":"b[iao]sh"}`, + match: []string{"bish", "bash", "bosh"}, + mismatch: []string{"woooosh", "fooo", "o", "fo", "boooooosh", "bsh", "bush"}, + }, + { + yaml: `{"type":"contains","value":"mba"}`, + match: []string{"bimbaz", "mba", "bi mba z"}, + mismatch: []string{"bim-baz"}, + }, + { + yaml: `{"type":"dateOlderThan","value":"0"}`, + match: []string{ + strconv.Itoa(int(future.Unix())), + future.Format("2006-01-02"), + future.Format("2006/01/02"), + future.Format("2006-01-02T15:04:05Z"), + future.Format("2006-01-02 15:04:05.000 +0000 UTC"), + future.Format(time.RFC3339Nano), + future.Format(time.RFC3339), + }, + mismatch: []string{ + "", + strconv.Itoa(int(past.Unix())), + past.Format("2006-01-02"), + past.Format("2006/01/02"), + past.Format("2006-01-02T15:04:05Z"), + past.Format("2006-01-02 15:04:05.14 -0700 MST"), + past.Format(time.RFC3339Nano), + past.Format(time.RFC3339), + }, + }, + } + + for _, tc := range cases { + t.Run(tc.yaml, func(t *testing.T) { + var filter config.Filter + + err := yaml.Unmarshal([]byte(tc.yaml), &filter) + if err != nil { + t.Fatal(err) + } + + for _, o := range tc.match { + match, err := filter.Match(o) + if err != nil { + t.Fatal(err) + } + + if !match { + t.Fatalf("'%v' should match", o) + } + } + + for _, o := range tc.mismatch { + match, err := filter.Match(o) + if err != nil { + t.Fatal(err) + } + + if match { + t.Fatalf("'%v' should not match", o) + } + } + }) + } +} diff --git a/pkg/types/properties.go b/pkg/types/properties.go new file mode 100644 index 00000000..bb2c27e9 --- /dev/null +++ b/pkg/types/properties.go @@ -0,0 +1,137 @@ +package types + +import ( + "fmt" + "sort" + "strings" +) + +type Properties map[string]string + +func NewProperties() Properties { + return make(Properties) +} + +func (p Properties) String() string { + parts := []string{} + for k, v := range p { + parts = append(parts, fmt.Sprintf(`%s: "%v"`, k, v)) + } + + sort.Strings(parts) + + return fmt.Sprintf("[%s]", strings.Join(parts, ", ")) +} + +func (p Properties) Set(key string, value interface{}) Properties { + if value == nil { + return p + } + + switch v := value.(type) { + case *string: + if v == nil { + return p + } + p[key] = *v + case []byte: + p[key] = string(v) + case *bool: + if v == nil { + return p + } + p[key] = fmt.Sprint(*v) + case *int64: + if v == nil { + return p + } + p[key] = fmt.Sprint(*v) + case *int: + if v == nil { + return p + } + p[key] = fmt.Sprint(*v) + default: + // Fallback to Stringer interface. This produces gibberish on pointers, + // but is the only way to avoid reflection. + p[key] = fmt.Sprint(value) + } + + return p +} + +func (p Properties) SetTag(tagKey *string, tagValue interface{}) Properties { + return p.SetTagWithPrefix("", tagKey, tagValue) +} + +func (p Properties) SetTagWithPrefix(prefix string, tagKey *string, tagValue interface{}) Properties { + if tagKey == nil { + return p + } + + keyStr := strings.TrimSpace(*tagKey) + prefix = strings.TrimSpace(prefix) + + if keyStr == "" { + return p + } + + if prefix != "" { + keyStr = fmt.Sprintf("%s:%s", prefix, keyStr) + } + + keyStr = fmt.Sprintf("tag:%s", keyStr) + + return p.Set(keyStr, tagValue) +} + +func (p Properties) SetPropertyWithPrefix(prefix string, propertyKey string, propertyValue interface{}) Properties { + keyStr := strings.TrimSpace(propertyKey) + prefix = strings.TrimSpace(prefix) + + if keyStr == "" { + return p + } + + if prefix != "" { + keyStr = fmt.Sprintf("%s:%s", prefix, keyStr) + } + + return p.Set(keyStr, propertyValue) +} + +func (p Properties) Get(key string) string { + value, ok := p[key] + if !ok { + return "" + } + + return value +} + +func (p Properties) Equals(o Properties) bool { + if p == nil && o == nil { + return true + } + + if p == nil || o == nil { + return false + } + + if len(p) != len(o) { + return false + } + + for k, pv := range p { + ov, ok := o[k] + if !ok { + return false + } + + if pv != ov { + return false + } + } + + return true +} diff --git a/pkg/types/properties_test.go b/pkg/types/properties_test.go new file mode 100644 index 00000000..6561d26d --- /dev/null +++ b/pkg/types/properties_test.go @@ -0,0 +1,201 @@ +package types_test + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +func TestPropertiesEquals(t *testing.T) { + cases := []struct { + p1, p2 types.Properties + result bool + }{ + { + p1: nil, + p2: nil, + result: true, + }, + { + p1: nil, + p2: types.NewProperties(), + result: false, + }, + { + p1: types.NewProperties(), + p2: types.NewProperties(), + result: true, + }, + { + p1: types.NewProperties().Set("blub", "blubber"), + p2: types.NewProperties().Set("blub", "blubber"), + result: true, + }, + { + p1: types.NewProperties().Set("blub", "foo"), + p2: types.NewProperties().Set("blub", "bar"), + result: false, + }, + { + p1: types.NewProperties().Set("bim", "baz").Set("blub", "blubber"), + p2: types.NewProperties().Set("bim", "baz").Set("blub", "blubber"), + result: true, + }, + { + p1: types.NewProperties().Set("bim", "baz").Set("blub", "foo"), + p2: types.NewProperties().Set("bim", "baz").Set("blub", "bar"), + result: false, + }, + } + + for i, tc := range cases { + t.Run(fmt.Sprint(i), func(t *testing.T) { + if tc.p1.Equals(tc.p2) != tc.result { + t.Errorf("Test Case failed. Want %t. Got %t.", !tc.result, tc.result) + t.Errorf("p1: %s", tc.p1.String()) + t.Errorf("p2: %s", tc.p2.String()) + } else if tc.p2.Equals(tc.p1) != tc.result { + t.Errorf("Test Case reverse check failed. Want %t. Got %t.", !tc.result, tc.result) + t.Errorf("p1: %s", tc.p1.String()) + t.Errorf("p2: %s", tc.p2.String()) + } + }) + } +} + +func TestPropertiesSetTag(t *testing.T) { + cases := []struct { + name string + key *string + value interface{} + want string + }{ + { + name: "string", + key: aws.String("name"), + value: "blubber", + want: `[tag:name: "blubber"]`, + }, + { + name: "string_ptr", + key: aws.String("name"), + value: aws.String("blubber"), + want: `[tag:name: "blubber"]`, + }, + { + name: "int", + key: aws.String("int"), + value: 42, + want: `[tag:int: "42"]`, + }, + { + name: "nil", + key: aws.String("nothing"), + value: nil, + want: `[]`, + }, + { + name: "empty_key", + key: aws.String(""), + value: "empty", + want: `[]`, + }, + { + name: "nil_key", + key: nil, + value: "empty", + want: `[]`, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + p := types.NewProperties() + + p.SetTag(tc.key, tc.value) + have := p.String() + + if tc.want != have { + t.Errorf("'%s' != '%s'", tc.want, have) + } + }) + } +} + +func TestPropertiesSetTagWithPrefix(t *testing.T) { + cases := []struct { + name string + prefix string + key *string + value interface{} + want string + }{ + { + name: "empty", + prefix: "", + key: aws.String("name"), + value: "blubber", + want: `[tag:name: "blubber"]`, + }, + { + name: "nonempty", + prefix: "bish", + key: aws.String("bash"), + value: "bosh", + want: `[tag:bish:bash: "bosh"]`, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + p := types.NewProperties() + + p.SetTagWithPrefix(tc.prefix, tc.key, tc.value) + have := p.String() + + if tc.want != have { + t.Errorf("'%s' != '%s'", tc.want, have) + } + }) + } +} + +func TestPropertiesSetPropertiesWithPrefix(t *testing.T) { + cases := []struct { + name string + prefix string + key string + value interface{} + want string + }{ + { + name: "empty", + prefix: "", + key: "OwnerID", + value: aws.String("123456789012"), + want: `[OwnerID: "123456789012"]`, + }, + { + name: "nonempty", + prefix: "igw", + key: "OwnerID", + value: aws.String("123456789012"), + want: `[igw:OwnerID: "123456789012"]`, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + p := types.NewProperties() + + p.SetPropertyWithPrefix(tc.prefix, tc.key, tc.value) + have := p.String() + + if tc.want != have { + t.Errorf("'%s' != '%s'", tc.want, have) + } + }) + } +} diff --git a/resources/athena-data-catalogs.go b/resources/athena-data-catalogs.go new file mode 100644 index 00000000..c59e833e --- /dev/null +++ b/resources/athena-data-catalogs.go @@ -0,0 +1,77 @@ +package resources + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/athena" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +type AthenaDataCatalog struct { + svc *athena.Athena + name *string +} + +func init() { + register("AthenaDataCatalog", ListAthenaDataCatalogs) +} + +func ListAthenaDataCatalogs(sess *session.Session) ([]Resource, error) { + svc := athena.New(sess) + resources := []Resource{} + + params := &athena.ListDataCatalogsInput{ + MaxResults: aws.Int64(50), + } + + for { + output, err := svc.ListDataCatalogs(params) + if err != nil { + return nil, err + } + + for _, catalog := range output.DataCatalogsSummary { + resources = append(resources, &AthenaDataCatalog{ + svc: svc, + name: catalog.CatalogName, + }) + } + + if output.NextToken == nil { + break + } + + params.NextToken = output.NextToken + } + + return resources, nil +} + +func (f *AthenaDataCatalog) Properties() types.Properties { + properties := types.NewProperties() + properties.Set("Name", f.name) + + return properties +} + +func (f *AthenaDataCatalog) Remove() error { + + _, err := f.svc.DeleteDataCatalog(&athena.DeleteDataCatalogInput{ + Name: f.name, + }) + + return err +} + +func (f *AthenaDataCatalog) Filter() error { + if *f.name == "AwsDataCatalog" { + return fmt.Errorf("cannot delete default data source") + } + return nil +} + +func (f *AthenaDataCatalog) String() string { + return *f.name +} diff --git a/resources/athena-prepared-statements.go b/resources/athena-prepared-statements.go new file mode 100644 index 00000000..0f0593d7 --- /dev/null +++ b/resources/athena-prepared-statements.go @@ -0,0 +1,80 @@ +package resources + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/athena" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +type AthenaPreparedStatement struct { + svc *athena.Athena + workGroup *string + name *string +} + +func init() { + register("AthenaPreparedStatement", ListAthenaPreparedStatements) +} + +func ListAthenaPreparedStatements(sess *session.Session) ([]Resource, error) { + svc := athena.New(sess) + resources := []Resource{} + + workgroups, err := svc.ListWorkGroups(&athena.ListWorkGroupsInput{}) + if err != nil { + return nil, err + } + + for _, workgroup := range workgroups.WorkGroups { + params := &athena.ListPreparedStatementsInput{ + WorkGroup: workgroup.Name, + MaxResults: aws.Int64(50), + } + + for { + output, err := svc.ListPreparedStatements(params) + if err != nil { + return nil, err + } + + for _, statement := range output.PreparedStatements { + resources = append(resources, &AthenaPreparedStatement{ + svc: svc, + workGroup: workgroup.Name, + name: statement.StatementName, + }) + } + + if output.NextToken == nil { + break + } + + params.NextToken = output.NextToken + } + } + + return resources, nil +} + +func (f *AthenaPreparedStatement) Properties() types.Properties { + properties := types.NewProperties() + properties.Set("StatementName", f.name) + properties.Set("WorkGroup", f.workGroup) + + return properties +} + +func (f *AthenaPreparedStatement) Remove() error { + + _, err := f.svc.DeletePreparedStatement(&athena.DeletePreparedStatementInput{ + StatementName: f.name, + WorkGroup: f.workGroup, + }) + + return err +} + +func (f *AthenaPreparedStatement) String() string { + return *f.name +} diff --git a/resources/autoscaling-groups.go b/resources/autoscaling-groups.go index 25eec09f..4f049291 100644 --- a/resources/autoscaling-groups.go +++ b/resources/autoscaling-groups.go @@ -43,7 +43,6 @@ func (l *AutoScalingGroupLister) List(_ context.Context, o interface{}) ([]resou } return !lastPage }) - if err != nil { return nil, err } @@ -81,7 +80,7 @@ func (asg *AutoScalingGroup) Properties() types.Properties { properties.SetTag(tag.Key, tag.Value) } - properties.Set("CreatedTime", asg.group.CreatedTime) + properties.Set("CreatedTime", asg.group.CreatedTime.Format(time.RFC3339)) properties.Set("Name", asg.group.AutoScalingGroupName) return properties diff --git a/resources/backup-vaults-access-policies.go b/resources/backup-vaults-access-policies.go index bff37090..4c599606 100644 --- a/resources/backup-vaults-access-policies.go +++ b/resources/backup-vaults-access-policies.go @@ -102,21 +102,31 @@ func (b *BackupVaultAccessPolicy) Remove(_ context.Context) error { // ] // } // - // While deletion is Denied, you can update the policy with one that - // doesn't deny and then delete at will. + // Update the default policy to remove the Deny on Delete* actions + // and then delete the policy. + // + // Why not putting a policy that allows `backup:DeleteBackupVaultAccessPolicy` in the first place? + // Because that throws an error: + // ' The specified policy cannot be added to the vault due to cross-account sharing restrictions. + // Amend the policy or the vault's settings, then retry request' + // allowDeletionPolicy := `{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": { - "AWS": "*" - }, - "Action": "backup:DeleteBackupVaultAccessPolicy", - "Resource": "*" - } - ] -}` + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Action": [ + "backup:StartCopyJob", + "backup:StartRestoreJob", + "backup:UpdateRecoveryPointLifecycle" + ], + "Resource": "*" + } + ] + }` // Ignore error from if we can't put permissive backup vault policy in for some reason, that's OK. _, _ = b.svc.PutBackupVaultAccessPolicy(&backup.PutBackupVaultAccessPolicyInput{ BackupVaultName: &b.backupVaultName, diff --git a/resources/bedrock-agentalias.go b/resources/bedrock-agentalias.go new file mode 100644 index 00000000..63e0edba --- /dev/null +++ b/resources/bedrock-agentalias.go @@ -0,0 +1,105 @@ +package resources + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/bedrockagent" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +type BedrockAgentAlias struct { + svc *bedrockagent.BedrockAgent + AgentId *string + AgentAliasId *string + AgentAliasName *string +} + +func init() { + register("BedrockAgentAlias", ListBedrockAgentAliases) +} + +func ListBedrockAgentAliases(sess *session.Session) ([]Resource, error) { + svc := bedrockagent.New(sess) + resources := []Resource{} + + agentIds, err := ListBedrockAgentIds(svc) + if err != nil { + return nil, err + } + + for _, agentId := range agentIds { + params := &bedrockagent.ListAgentAliasesInput{ + MaxResults: aws.Int64(100), + AgentId: aws.String(agentId), + } + for { + output, err := svc.ListAgentAliases(params) + if err != nil { + return nil, err + } + + for _, agentAliasInfo := range output.AgentAliasSummaries { + resources = append(resources, &BedrockAgentAlias{ + svc: svc, + AgentId: aws.String(agentId), + AgentAliasName: agentAliasInfo.AgentAliasName, + AgentAliasId: agentAliasInfo.AgentAliasId, + }) + } + + if output.NextToken == nil { + break + } + params.NextToken = output.NextToken + } + + } + + return resources, nil +} + +func ListBedrockAgentIds(svc *bedrockagent.BedrockAgent) ([]string, error) { + + agentIds := []string{} + params := &bedrockagent.ListAgentsInput{ + MaxResults: aws.Int64(100), + } + for { + output, err := svc.ListAgents(params) + if err != nil { + return nil, err + } + + for _, agent := range output.AgentSummaries { + agentIds = append(agentIds, *agent.AgentId) + } + + if output.NextToken == nil { + break + } + params.NextToken = output.NextToken + } + + return agentIds, nil +} + +func (f *BedrockAgentAlias) Remove() error { + _, err := f.svc.DeleteAgentAlias(&bedrockagent.DeleteAgentAliasInput{ + AgentAliasId: f.AgentAliasId, + AgentId: f.AgentId, + }) + return err +} + +func (f *BedrockAgentAlias) Properties() types.Properties { + properties := types.NewProperties(). + Set("AgentId", f.AgentId). + Set("AgentAliasId", f.AgentAliasId). + Set("AgentAliasName", f.AgentAliasName) + + return properties +} + +func (f *BedrockAgentAlias) String() string { + return *f.AgentAliasName +} diff --git a/resources/bedrock-flowalias.go b/resources/bedrock-flowalias.go new file mode 100644 index 00000000..eca852ca --- /dev/null +++ b/resources/bedrock-flowalias.go @@ -0,0 +1,115 @@ +package resources + +import ( + "fmt" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/bedrockagent" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +type BedrockFlowAlias struct { + svc *bedrockagent.BedrockAgent + FlowId *string + FlowAliasId *string + FlowAliasName *string +} + +func init() { + register("BedrockFlowAlias", ListBedrockFlowAliases) +} + +func ListBedrockFlowAliases(sess *session.Session) ([]Resource, error) { + svc := bedrockagent.New(sess) + resources := []Resource{} + + flowIds, err := ListBedrockFlowIds(svc) + if err != nil { + return nil, err + } + + for _, flowId := range flowIds { + params := &bedrockagent.ListFlowAliasesInput{ + MaxResults: aws.Int64(100), + FlowIdentifier: aws.String(flowId), + } + for { + output, err := svc.ListFlowAliases(params) + if err != nil { + return nil, err + } + + for _, flowAliasInfo := range output.FlowAliasSummaries { + resources = append(resources, &BedrockFlowAlias{ + svc: svc, + FlowId: flowAliasInfo.FlowId, + FlowAliasId: flowAliasInfo.Id, + FlowAliasName: flowAliasInfo.Name, + }) + } + + if output.NextToken == nil { + break + } + params.NextToken = output.NextToken + } + + } + + return resources, nil +} + +func ListBedrockFlowIds(svc *bedrockagent.BedrockAgent) ([]string, error) { + + flowIds := []string{} + params := &bedrockagent.ListFlowsInput{ + MaxResults: aws.Int64(100), + } + for { + output, err := svc.ListFlows(params) + if err != nil { + return nil, err + } + + for _, flow := range output.FlowSummaries { + flowIds = append(flowIds, *flow.Id) + } + + if output.NextToken == nil { + break + } + params.NextToken = output.NextToken + } + + return flowIds, nil +} + +func (f *BedrockFlowAlias) Filter() error { + if strings.HasPrefix(*f.FlowAliasName, "TSTALIASID") { + return fmt.Errorf("cannot delete AWS managed Flow Alias") + } + return nil +} + +func (f *BedrockFlowAlias) Remove() error { + _, err := f.svc.DeleteFlowAlias(&bedrockagent.DeleteFlowAliasInput{ + AliasIdentifier: f.FlowAliasId, + FlowIdentifier: f.FlowId, + }) + return err +} + +func (f *BedrockFlowAlias) Properties() types.Properties { + properties := types.NewProperties(). + Set("FlowId", f.FlowId). + Set("FlowAliasId", f.FlowAliasId). + Set("FlowAliasName", f.FlowAliasName) + + return properties +} + +func (f *BedrockFlowAlias) String() string { + return *f.FlowAliasName +} diff --git a/resources/cloudwatch-anomaly-detectors.go b/resources/cloudwatch-anomaly-detectors.go new file mode 100644 index 00000000..d39543df --- /dev/null +++ b/resources/cloudwatch-anomaly-detectors.go @@ -0,0 +1,66 @@ +package resources + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/cloudwatch" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +type CloudWatchAnomalyDetector struct { + svc *cloudwatch.CloudWatch + detector *cloudwatch.AnomalyDetector +} + +func init() { + register("CloudWatchAnomalyDetector", ListCloudWatchAnomalyDetectors) +} + +func ListCloudWatchAnomalyDetectors(sess *session.Session) ([]Resource, error) { + svc := cloudwatch.New(sess) + resources := []Resource{} + + params := &cloudwatch.DescribeAnomalyDetectorsInput{ + MaxResults: aws.Int64(25), + } + + for { + output, err := svc.DescribeAnomalyDetectors(params) + if err != nil { + return nil, err + } + + for _, detector := range output.AnomalyDetectors { + resources = append(resources, &CloudWatchAnomalyDetector{ + svc: svc, + detector: detector, + }) + } + + if output.NextToken == nil { + break + } + + params.NextToken = output.NextToken + } + + return resources, nil +} + +func (f *CloudWatchAnomalyDetector) Remove() error { + _, err := f.svc.DeleteAnomalyDetector(&cloudwatch.DeleteAnomalyDetectorInput{ + SingleMetricAnomalyDetector: f.detector.SingleMetricAnomalyDetector, + }) + + return err +} + +func (f *CloudWatchAnomalyDetector) Properties() types.Properties { + properties := types.NewProperties() + properties.Set("MetricName", f.detector.SingleMetricAnomalyDetector.MetricName) + return properties +} + +func (f *CloudWatchAnomalyDetector) String() string { + return *f.detector.SingleMetricAnomalyDetector.MetricName +} diff --git a/resources/cloudwatch-insight-rules.go b/resources/cloudwatch-insight-rules.go new file mode 100644 index 00000000..3b1519b9 --- /dev/null +++ b/resources/cloudwatch-insight-rules.go @@ -0,0 +1,66 @@ +package resources + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/cloudwatch" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +type CloudWatchInsightRule struct { + svc *cloudwatch.CloudWatch + name *string +} + +func init() { + register("CloudWatchInsightRule", ListCloudWatchInsightRules) +} + +func ListCloudWatchInsightRules(sess *session.Session) ([]Resource, error) { + svc := cloudwatch.New(sess) + resources := []Resource{} + + params := &cloudwatch.DescribeInsightRulesInput{ + MaxResults: aws.Int64(25), + } + + for { + output, err := svc.DescribeInsightRules(params) + if err != nil { + return nil, err + } + + for _, rules := range output.InsightRules { + resources = append(resources, &CloudWatchInsightRule{ + svc: svc, + name: rules.Name, + }) + } + + if output.NextToken == nil { + break + } + + params.NextToken = output.NextToken + } + + return resources, nil +} + +func (f *CloudWatchInsightRule) Remove() error { + _, err := f.svc.DeleteInsightRules(&cloudwatch.DeleteInsightRulesInput{ + RuleNames: []*string{f.name}, + }) + + return err +} + +func (f *CloudWatchInsightRule) Properties() types.Properties { + properties := types.NewProperties() + properties.Set("Name", f.name) + return properties +} + +func (f *CloudWatchInsightRule) String() string { + return *f.name +} diff --git a/resources/codebuild-builds.go b/resources/codebuild-builds.go new file mode 100644 index 00000000..e6e10810 --- /dev/null +++ b/resources/codebuild-builds.go @@ -0,0 +1,60 @@ +package resources + +import ( + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/codebuild" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +type CodeBuildBuild struct { + svc *codebuild.CodeBuild + Id *string +} + +func init() { + register("CodeBuildBuild", ListCodeBuildBuild) +} + +func ListCodeBuildBuild(sess *session.Session) ([]Resource, error) { + svc := codebuild.New(sess) + resources := []Resource{} + + params := &codebuild.ListBuildsInput{} + + for { + resp, err := svc.ListBuilds(params) + if err != nil { + return nil, err + } + + for _, build := range resp.Ids { + resources = append(resources, &CodeBuildBuild{ + svc: svc, + Id: build, + }) + } + + if resp.NextToken == nil { + break + } + + params.NextToken = resp.NextToken + } + + return resources, nil +} + +func (f *CodeBuildBuild) Remove() error { + _, err := f.svc.BatchDeleteBuilds(&codebuild.BatchDeleteBuildsInput{ + Ids: []*string{f.Id}, + }) + + return err +} + +func (f *CodeBuildBuild) Properties() types.Properties { + properties := types.NewProperties() + properties. + Set("Id", f.Id) + return properties +} diff --git a/resources/codedeploy-deployment-configs.go b/resources/codedeploy-deployment-configs.go new file mode 100644 index 00000000..5682df16 --- /dev/null +++ b/resources/codedeploy-deployment-configs.go @@ -0,0 +1,73 @@ +package resources + +import ( + "fmt" + "strings" + + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/codedeploy" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +type CodeDeployDeploymentConfig struct { + svc *codedeploy.CodeDeploy + deploymentConfigName *string +} + +func init() { + register("CodeDeployDeploymentConfig", ListCodeDeployDeploymentConfigs, mapCloudControl("AWS::CodeDeploy::DeploymentConfig")) +} + +func ListCodeDeployDeploymentConfigs(sess *session.Session) ([]Resource, error) { + svc := codedeploy.New(sess) + resources := []Resource{} + + params := &codedeploy.ListDeploymentConfigsInput{} + + for { + resp, err := svc.ListDeploymentConfigs(params) + if err != nil { + return nil, err + } + + for _, config := range resp.DeploymentConfigsList { + resources = append(resources, &CodeDeployDeploymentConfig{ + svc: svc, + deploymentConfigName: config, + }) + } + + if resp.NextToken == nil { + break + } + + params.NextToken = resp.NextToken + } + + return resources, nil +} + +func (f *CodeDeployDeploymentConfig) Filter() error { + if strings.HasPrefix(*f.deploymentConfigName, "CodeDeployDefault") { + return fmt.Errorf("cannot delete default codedeploy config") + } + return nil +} + +func (f *CodeDeployDeploymentConfig) Remove() error { + _, err := f.svc.DeleteDeploymentConfig(&codedeploy.DeleteDeploymentConfigInput{ + DeploymentConfigName: f.deploymentConfigName, + }) + + return err +} + +func (f *CodeDeployDeploymentConfig) Properties() types.Properties { + properties := types.NewProperties() + properties.Set("DeploymentConfigName", f.deploymentConfigName) + return properties +} + +func (f *CodeDeployDeploymentConfig) String() string { + return *f.deploymentConfigName +} diff --git a/resources/codedeploy-deployment-groups.go b/resources/codedeploy-deployment-groups.go new file mode 100644 index 00000000..67616a4d --- /dev/null +++ b/resources/codedeploy-deployment-groups.go @@ -0,0 +1,69 @@ +package resources + +import ( + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/codedeploy" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +type CodeDeployDeploymentGroup struct { + svc *codedeploy.CodeDeploy + deploymentGroupName *string + applicationName *string +} + +func init() { + register("CodeDeployDeploymentGroup", ListCodeDeployDeploymentGroups) +} + +func ListCodeDeployDeploymentGroups(sess *session.Session) ([]Resource, error) { + svc := codedeploy.New(sess) + resources := []Resource{} + + appParams := &codedeploy.ListApplicationsInput{} + appResp, err := svc.ListApplications(appParams) + if err != nil { + return nil, err + } + + for _, appName := range appResp.Applications { + // For each application, list deployment groups + deploymentGroupParams := &codedeploy.ListDeploymentGroupsInput{ + ApplicationName: appName, + } + deploymentGroupResp, err := svc.ListDeploymentGroups(deploymentGroupParams) + if err != nil { + return nil, err + } + + for _, group := range deploymentGroupResp.DeploymentGroups { + resources = append(resources, &CodeDeployDeploymentGroup{ + svc: svc, + deploymentGroupName: group, + applicationName: appName, + }) + } + } + + return resources, nil +} + +func (f *CodeDeployDeploymentGroup) Remove() error { + _, err := f.svc.DeleteDeploymentGroup(&codedeploy.DeleteDeploymentGroupInput{ + ApplicationName: f.applicationName, + DeploymentGroupName: f.deploymentGroupName, + }) + + return err +} + +func (f *CodeDeployDeploymentGroup) Properties() types.Properties { + properties := types.NewProperties() + properties.Set("DeploymentGroupName", f.deploymentGroupName) + properties.Set("ApplicationName", f.applicationName) + return properties +} + +func (f *CodeDeployDeploymentGroup) String() string { + return *f.deploymentGroupName +} diff --git a/resources/codegurureviewer-repository-associations.go b/resources/codegurureviewer-repository-associations.go new file mode 100644 index 00000000..e630ad94 --- /dev/null +++ b/resources/codegurureviewer-repository-associations.go @@ -0,0 +1,72 @@ +package resources + +import ( + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/codegurureviewer" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +type CodeGuruReviewerRepositoryAssociation struct { + svc *codegurureviewer.CodeGuruReviewer + AssociationArn *string + AssociationId *string + Name *string + Owner *string + ProviderType *string +} + +func init() { + register("CodeGuruReviewerRepositoryAssociation", ListCodeGuruReviewerRepositoryAssociations, + mapCloudControl("AWS::CodeGuruReviewer::RepositoryAssociation")) +} + +func ListCodeGuruReviewerRepositoryAssociations(sess *session.Session) ([]Resource, error) { + svc := codegurureviewer.New(sess) + resources := []Resource{} + + params := &codegurureviewer.ListRepositoryAssociationsInput{} + + for { + resp, err := svc.ListRepositoryAssociations(params) + if err != nil { + return nil, err + } + + for _, association := range resp.RepositoryAssociationSummaries { + resources = append(resources, &CodeGuruReviewerRepositoryAssociation{ + svc: svc, + AssociationArn: association.AssociationArn, + AssociationId: association.AssociationId, + Name: association.Name, + Owner: association.Owner, + ProviderType: association.ProviderType, + }) + } + + if resp.NextToken == nil { + break + } + + params.NextToken = resp.NextToken + } + + return resources, nil +} + +func (f *CodeGuruReviewerRepositoryAssociation) Remove() error { + _, err := f.svc.DisassociateRepository(&codegurureviewer.DisassociateRepositoryInput{ + AssociationArn: f.AssociationArn, + }) + return err +} + +func (f *CodeGuruReviewerRepositoryAssociation) Properties() types.Properties { + properties := types.NewProperties() + properties. + Set("AssociationArn", f.AssociationArn) + properties.Set("AssociationId", f.AssociationId) + properties.Set("Name", f.Name) + properties.Set("Owner", f.Owner) + properties.Set("ProviderType", f.ProviderType) + return properties +} diff --git a/resources/codepipeline-custom-action-types.go b/resources/codepipeline-custom-action-types.go new file mode 100644 index 00000000..97a7903b --- /dev/null +++ b/resources/codepipeline-custom-action-types.go @@ -0,0 +1,84 @@ +package resources + +import ( + "fmt" + "strings" + + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/codepipeline" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +type CodePipelineCustomActionType struct { + svc *codepipeline.CodePipeline + owner *string + category *string + provider *string + version *string +} + +func init() { + register("CodePipelineCustomActionType", ListCodePipelineCustomActionTypes, mapCloudControl("AWS::CodePipeline::CustomActionType")) +} + +func ListCodePipelineCustomActionTypes(sess *session.Session) ([]Resource, error) { + svc := codepipeline.New(sess) + resources := []Resource{} + + params := &codepipeline.ListActionTypesInput{} + + for { + resp, err := svc.ListActionTypes(params) + if err != nil { + return nil, err + } + + for _, actionTypes := range resp.ActionTypes { + resources = append(resources, &CodePipelineCustomActionType{ + svc: svc, + owner: actionTypes.Id.Owner, + category: actionTypes.Id.Category, + provider: actionTypes.Id.Provider, + version: actionTypes.Id.Version, + }) + } + + if resp.NextToken == nil { + break + } + + params.NextToken = resp.NextToken + } + + return resources, nil +} + +func (f *CodePipelineCustomActionType) Filter() error { + if !strings.HasPrefix(*f.owner, "Custom") { + return fmt.Errorf("cannot delete default codepipeline custom action type") + } + return nil +} + +func (f *CodePipelineCustomActionType) Remove() error { + _, err := f.svc.DeleteCustomActionType(&codepipeline.DeleteCustomActionTypeInput{ + Category: f.category, + Provider: f.provider, + Version: f.version, + }) + + return err +} + +func (f *CodePipelineCustomActionType) Properties() types.Properties { + properties := types.NewProperties() + properties.Set("Category", f.category) + properties.Set("Owner", f.owner) + properties.Set("Provider", f.provider) + properties.Set("Version", f.version) + return properties +} + +func (f *CodePipelineCustomActionType) String() string { + return *f.owner +} diff --git a/resources/codepipeline-webhooks.go b/resources/codepipeline-webhooks.go new file mode 100644 index 00000000..508894fe --- /dev/null +++ b/resources/codepipeline-webhooks.go @@ -0,0 +1,63 @@ +package resources + +import ( + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/codepipeline" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +type CodePipelineWebhook struct { + svc *codepipeline.CodePipeline + name *string +} + +func init() { + register("CodePipelineWebhook", ListCodePipelineWebhooks) +} + +func ListCodePipelineWebhooks(sess *session.Session) ([]Resource, error) { + svc := codepipeline.New(sess) + resources := []Resource{} + + params := &codepipeline.ListWebhooksInput{} + + for { + resp, err := svc.ListWebhooks(params) + if err != nil { + return nil, err + } + + for _, webHooks := range resp.Webhooks { + resources = append(resources, &CodePipelineWebhook{ + svc: svc, + name: webHooks.Definition.Name, + }) + } + + if resp.NextToken == nil { + break + } + + params.NextToken = resp.NextToken + } + + return resources, nil +} + +func (f *CodePipelineWebhook) Remove() error { + _, err := f.svc.DeleteWebhook(&codepipeline.DeleteWebhookInput{ + Name: f.name, + }) + + return err +} + +func (f *CodePipelineWebhook) Properties() types.Properties { + properties := types.NewProperties() + properties.Set("Name", f.name) + return properties +} + +func (f *CodePipelineWebhook) String() string { + return *f.name +} diff --git a/resources/comprehend_events_detection_job.go b/resources/comprehend_events_detection_job.go new file mode 100644 index 00000000..e8b303eb --- /dev/null +++ b/resources/comprehend_events_detection_job.go @@ -0,0 +1,72 @@ +package resources + +import ( + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/comprehend" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +func init() { + register("ComprehendEventsDetectionJob", ListComprehendEventsDetectionJobs) +} + +func ListComprehendEventsDetectionJobs(sess *session.Session) ([]Resource, error) { + svc := comprehend.New(sess) + + params := &comprehend.ListEventsDetectionJobsInput{} + resources := make([]Resource, 0) + + for { + resp, err := svc.ListEventsDetectionJobs(params) + if err != nil { + return nil, err + } + for _, eventsDetectionJob := range resp.EventsDetectionJobPropertiesList { + switch *eventsDetectionJob.JobStatus { + case "STOPPED", "FAILED", "COMPLETED": + // if the job has already been stopped, failed, or completed; do not try to stop it again + continue + } + resources = append(resources, &ComprehendEventsDetectionJob{ + svc: svc, + eventsDetectionJob: eventsDetectionJob, + }) + } + + if resp.NextToken == nil { + break + } + + params.NextToken = resp.NextToken + } + + return resources, nil +} + +type ComprehendEventsDetectionJob struct { + svc *comprehend.Comprehend + eventsDetectionJob *comprehend.EventsDetectionJobProperties +} + +func (ce *ComprehendEventsDetectionJob) Remove() error { + _, err := ce.svc.StopEventsDetectionJob(&comprehend.StopEventsDetectionJobInput{ + JobId: ce.eventsDetectionJob.JobId, + }) + return err +} + +func (ce *ComprehendEventsDetectionJob) Properties() types.Properties { + properties := types.NewProperties() + properties.Set("JobName", ce.eventsDetectionJob.JobName) + properties.Set("JobId", ce.eventsDetectionJob.JobId) + + return properties +} + +func (ce *ComprehendEventsDetectionJob) String() string { + if ce.eventsDetectionJob.JobName == nil { + return "Unnamed job" + } else { + return *ce.eventsDetectionJob.JobName + } +} diff --git a/resources/comprehend_pii_entities_detection_job.go b/resources/comprehend_pii_entities_detection_job.go new file mode 100644 index 00000000..6d923461 --- /dev/null +++ b/resources/comprehend_pii_entities_detection_job.go @@ -0,0 +1,72 @@ +package resources + +import ( + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/comprehend" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +func init() { + register("ComprehendPiiEntititesDetectionJob", ListComprehendPiiEntitiesDetectionJobs) +} + +func ListComprehendPiiEntitiesDetectionJobs(sess *session.Session) ([]Resource, error) { + svc := comprehend.New(sess) + + params := &comprehend.ListPiiEntitiesDetectionJobsInput{} + resources := make([]Resource, 0) + + for { + resp, err := svc.ListPiiEntitiesDetectionJobs(params) + if err != nil { + return nil, err + } + for _, piiEntititesDetectionJob := range resp.PiiEntitiesDetectionJobPropertiesList { + switch *piiEntititesDetectionJob.JobStatus { + case "STOPPED", "FAILED", "COMPLETED": + // if the job has already been stopped, failed, or completed; do not try to stop it again + continue + } + resources = append(resources, &ComprehendPiiEntitiesDetectionJob{ + svc: svc, + piiEntititesDetectionJob: piiEntititesDetectionJob, + }) + } + + if resp.NextToken == nil { + break + } + + params.NextToken = resp.NextToken + } + + return resources, nil +} + +type ComprehendPiiEntitiesDetectionJob struct { + svc *comprehend.Comprehend + piiEntititesDetectionJob *comprehend.PiiEntitiesDetectionJobProperties +} + +func (ce *ComprehendPiiEntitiesDetectionJob) Remove() error { + _, err := ce.svc.StopPiiEntitiesDetectionJob(&comprehend.StopPiiEntitiesDetectionJobInput{ + JobId: ce.piiEntititesDetectionJob.JobId, + }) + return err +} + +func (ce *ComprehendPiiEntitiesDetectionJob) Properties() types.Properties { + properties := types.NewProperties() + properties.Set("JobName", ce.piiEntititesDetectionJob.JobName) + properties.Set("JobId", ce.piiEntititesDetectionJob.JobId) + + return properties +} + +func (ce *ComprehendPiiEntitiesDetectionJob) String() string { + if ce.piiEntititesDetectionJob.JobName == nil { + return "Unnamed job" + } else { + return *ce.piiEntititesDetectionJob.JobName + } +} diff --git a/resources/comprehend_targeted_sentiment_detection_job.go b/resources/comprehend_targeted_sentiment_detection_job.go new file mode 100644 index 00000000..b60b39b6 --- /dev/null +++ b/resources/comprehend_targeted_sentiment_detection_job.go @@ -0,0 +1,72 @@ +package resources + +import ( + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/comprehend" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +func init() { + register("ComprehendTargetedSentimentDetectionJob", ListComprehendTargetedSentimentDetectionJobs) +} + +func ListComprehendTargetedSentimentDetectionJobs(sess *session.Session) ([]Resource, error) { + svc := comprehend.New(sess) + + params := &comprehend.ListTargetedSentimentDetectionJobsInput{} + resources := make([]Resource, 0) + + for { + resp, err := svc.ListTargetedSentimentDetectionJobs(params) + if err != nil { + return nil, err + } + for _, targetedSentimentDetectionJob := range resp.TargetedSentimentDetectionJobPropertiesList { + switch *targetedSentimentDetectionJob.JobStatus { + case "STOPPED", "FAILED", "COMPLETED": + // if the job has already been stopped, failed, or completed; do not try to stop it again + continue + } + resources = append(resources, &ComprehendTargetedSentimentDetectionJob{ + svc: svc, + targetedSentimentDetectionJob: targetedSentimentDetectionJob, + }) + } + + if resp.NextToken == nil { + break + } + + params.NextToken = resp.NextToken + } + + return resources, nil +} + +type ComprehendTargetedSentimentDetectionJob struct { + svc *comprehend.Comprehend + targetedSentimentDetectionJob *comprehend.TargetedSentimentDetectionJobProperties +} + +func (ce *ComprehendTargetedSentimentDetectionJob) Remove() error { + _, err := ce.svc.StopTargetedSentimentDetectionJob(&comprehend.StopTargetedSentimentDetectionJobInput{ + JobId: ce.targetedSentimentDetectionJob.JobId, + }) + return err +} + +func (ce *ComprehendTargetedSentimentDetectionJob) Properties() types.Properties { + properties := types.NewProperties() + properties.Set("JobName", ce.targetedSentimentDetectionJob.JobName) + properties.Set("JobId", ce.targetedSentimentDetectionJob.JobId) + + return properties +} + +func (ce *ComprehendTargetedSentimentDetectionJob) String() string { + if ce.targetedSentimentDetectionJob.JobName == nil { + return "Unnamed job" + } else { + return *ce.targetedSentimentDetectionJob.JobName + } +} diff --git a/resources/configservice-configrules.go b/resources/configservice-configrules.go new file mode 100644 index 00000000..e185b207 --- /dev/null +++ b/resources/configservice-configrules.go @@ -0,0 +1,60 @@ +package resources + +import ( + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/configservice" +) + +type ConfigServiceConfigRule struct { + svc *configservice.ConfigService + configRuleName *string +} + +func init() { + register("ConfigServiceConfigRule", ListConfigServiceConfigRules) +} + +func ListConfigServiceConfigRules(sess *session.Session) ([]Resource, error) { + svc := configservice.New(sess) + resources := []Resource{} + + params := &configservice.DescribeConfigRulesInput{} + + for { + output, err := svc.DescribeConfigRules(params) + if err != nil { + return nil, err + } + + for _, configRule := range output.ConfigRules { + resources = append(resources, &ConfigServiceConfigRule{ + svc: svc, + configRuleName: configRule.ConfigRuleName, + }) + } + + if output.NextToken == nil { + break + } + + params.NextToken = output.NextToken + } + + return resources, nil +} + +func (f *ConfigServiceConfigRule) Remove() error { + f.svc.DeleteRemediationConfiguration(&configservice.DeleteRemediationConfigurationInput{ + ConfigRuleName: f.configRuleName, + }) + + _, err := f.svc.DeleteConfigRule(&configservice.DeleteConfigRuleInput{ + ConfigRuleName: f.configRuleName, + }) + + return err +} + +func (f *ConfigServiceConfigRule) String() string { + return *f.configRuleName +} diff --git a/resources/configservice-configurationrecorders.go b/resources/configservice-configurationrecorders.go new file mode 100644 index 00000000..c0d95809 --- /dev/null +++ b/resources/configservice-configurationrecorders.go @@ -0,0 +1,48 @@ +package resources + +import ( + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/configservice" +) + +type ConfigServiceConfigurationRecorder struct { + svc *configservice.ConfigService + configurationRecorderName *string +} + +func init() { + register("ConfigServiceConfigurationRecorder", ListConfigServiceConfigurationRecorders) +} + +func ListConfigServiceConfigurationRecorders(sess *session.Session) ([]Resource, error) { + svc := configservice.New(sess) + + params := &configservice.DescribeConfigurationRecordersInput{} + resp, err := svc.DescribeConfigurationRecorders(params) + if err != nil { + return nil, err + } + + resources := make([]Resource, 0) + for _, configurationRecorder := range resp.ConfigurationRecorders { + resources = append(resources, &ConfigServiceConfigurationRecorder{ + svc: svc, + configurationRecorderName: configurationRecorder.Name, + }) + } + + return resources, nil +} + +func (f *ConfigServiceConfigurationRecorder) Remove() error { + + _, err := f.svc.DeleteConfigurationRecorder(&configservice.DeleteConfigurationRecorderInput{ + ConfigurationRecorderName: f.configurationRecorderName, + }) + + return err +} + +func (f *ConfigServiceConfigurationRecorder) String() string { + return *f.configurationRecorderName +} diff --git a/resources/dax-subnetgroups.go b/resources/dax-subnetgroups.go new file mode 100644 index 00000000..81fc63c5 --- /dev/null +++ b/resources/dax-subnetgroups.go @@ -0,0 +1,68 @@ +package resources + +import ( + "fmt" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/dax" +) + +type DAXSubnetGroup struct { + svc *dax.DAX + subnetGroupName *string +} + +func init() { + register("DAXSubnetGroup", ListDAXSubnetGroups) +} + +func ListDAXSubnetGroups(sess *session.Session) ([]Resource, error) { + svc := dax.New(sess) + resources := []Resource{} + + params := &dax.DescribeSubnetGroupsInput{ + MaxResults: aws.Int64(100), + } + + for { + output, err := svc.DescribeSubnetGroups(params) + if err != nil { + return nil, err + } + + for _, subnet := range output.SubnetGroups { + resources = append(resources, &DAXSubnetGroup{ + svc: svc, + subnetGroupName: subnet.SubnetGroupName, + }) + } + + if output.NextToken == nil { + break + } + + params.NextToken = output.NextToken + } + + return resources, nil +} + +func (f *DAXSubnetGroup) Filter() error { + if *f.subnetGroupName == "default" { + return fmt.Errorf("Cannot delete default DAX Subnet group") + } + return nil +} + +func (f *DAXSubnetGroup) Remove() error { + + _, err := f.svc.DeleteSubnetGroup(&dax.DeleteSubnetGroupInput{ + SubnetGroupName: f.subnetGroupName, + }) + + return err +} + +func (f *DAXSubnetGroup) String() string { + return *f.subnetGroupName +} diff --git a/resources/dynamodb-backups.go b/resources/dynamodb-backups.go new file mode 100644 index 00000000..540046a4 --- /dev/null +++ b/resources/dynamodb-backups.go @@ -0,0 +1,73 @@ +package resources + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +type DynamoDBBackup struct { + svc *dynamodb.DynamoDB + id string +} + +func init() { + register("DynamoDBBackup", ListDynamoDBBackups) +} + +func ListDynamoDBBackups(sess *session.Session) ([]Resource, error) { + svc := dynamodb.New(sess) + + resources := make([]Resource, 0) + + var lastEvaluatedBackupArn *string + + for { + backupsResp, err := svc.ListBackups(&dynamodb.ListBackupsInput{ + ExclusiveStartBackupArn: lastEvaluatedBackupArn, + }) + if err != nil { + return nil, err + } + + for _, backup := range backupsResp.BackupSummaries { + resources = append(resources, &DynamoDBBackup{ + svc: svc, + id: *backup.BackupArn, + }) + } + + if backupsResp.LastEvaluatedBackupArn == nil { + break + } + + lastEvaluatedBackupArn = backupsResp.LastEvaluatedBackupArn + } + + return resources, nil +} + +func (i *DynamoDBBackup) Remove() error { + params := &dynamodb.DeleteBackupInput{ + BackupArn: aws.String(i.id), + } + + _, err := i.svc.DeleteBackup(params) + if err != nil { + return err + } + + return nil +} + +func (i *DynamoDBBackup) Properties() types.Properties { + properties := types.NewProperties() + properties.Set("Identifier", i.id) + + return properties +} + +func (i *DynamoDBBackup) String() string { + return i.id +} diff --git a/resources/dynamodb-tables.go b/resources/dynamodb-tables.go new file mode 100644 index 00000000..cb191e61 --- /dev/null +++ b/resources/dynamodb-tables.go @@ -0,0 +1,92 @@ +package resources + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +type DynamoDBTable struct { + svc *dynamodb.DynamoDB + id string + tags []*dynamodb.Tag +} + +func init() { + register("DynamoDBTable", ListDynamoDBTables) +} + +func ListDynamoDBTables(sess *session.Session) ([]Resource, error) { + svc := dynamodb.New(sess) + + resp, err := svc.ListTables(&dynamodb.ListTablesInput{}) + if err != nil { + return nil, err + } + + resources := make([]Resource, 0) + for _, tableName := range resp.TableNames { + tags, err := GetTableTags(svc, tableName) + + if err != nil { + continue + } + + resources = append(resources, &DynamoDBTable{ + svc: svc, + id: *tableName, + tags: tags, + }) + } + + return resources, nil +} + +func (i *DynamoDBTable) Remove() error { + params := &dynamodb.DeleteTableInput{ + TableName: aws.String(i.id), + } + + _, err := i.svc.DeleteTable(params) + if err != nil { + return err + } + + return nil +} + +func GetTableTags(svc *dynamodb.DynamoDB, tableName *string) ([]*dynamodb.Tag, error) { + result, err := svc.DescribeTable(&dynamodb.DescribeTableInput{ + TableName: aws.String(*tableName), + }) + + if err != nil { + return make([]*dynamodb.Tag, 0), err + } + + tags, err := svc.ListTagsOfResource(&dynamodb.ListTagsOfResourceInput{ + ResourceArn: result.Table.TableArn, + }) + + if err != nil { + return make([]*dynamodb.Tag, 0), err + } + + return tags.Tags, nil +} + +func (i *DynamoDBTable) Properties() types.Properties { + properties := types.NewProperties() + properties.Set("Identifier", i.id) + + for _, tag := range i.tags { + properties.SetTag(tag.Key, tag.Value) + } + + return properties +} + +func (i *DynamoDBTable) String() string { + return i.id +} diff --git a/resources/ec2-default-security-group-rule.go b/resources/ec2-default-security-group-rule.go index 8c162998..cd5c0078 100644 --- a/resources/ec2-default-security-group-rule.go +++ b/resources/ec2-default-security-group-rule.go @@ -71,6 +71,7 @@ func (l *EC2DefaultSecurityGroupRuleLister) List(_ context.Context, o interface{ id: rule.SecurityGroupRuleId, groupID: rule.GroupId, isEgress: rule.IsEgress, + tags: rule.Tags, }) } return !lastPage diff --git a/resources/ec2-tgw-attachments.go b/resources/ec2-tgw-attachments.go index 4a3d4796..01dda08f 100644 --- a/resources/ec2-tgw-attachments.go +++ b/resources/ec2-tgw-attachments.go @@ -69,6 +69,21 @@ func (e *EC2TGWAttachment) Remove(_ context.Context) error { // as part of TGW to delete VPN attachments. return fmt.Errorf("VPN attachment") } + + // Execute different API calls depending on the resource type. + if *e.tgwa.ResourceType == "peering" { + params := &ec2.DeleteTransitGatewayPeeringAttachmentInput{ + TransitGatewayAttachmentId: e.tgwa.TransitGatewayAttachmentId, + } + + _, err := e.svc.DeleteTransitGatewayPeeringAttachment(params) + if err != nil { + return err + } + + return nil + } + params := &ec2.DeleteTransitGatewayVpcAttachmentInput{ TransitGatewayAttachmentId: e.tgwa.TransitGatewayAttachmentId, } diff --git a/resources/ecs-services.go b/resources/ecs-services.go index d15ab5db..adeab72e 100644 --- a/resources/ecs-services.go +++ b/resources/ecs-services.go @@ -60,24 +60,19 @@ func (l *ECSServiceLister) List(_ context.Context, o interface{}) ([]resource.Re Cluster: clusterArn, MaxResults: aws.Int64(10), } - output, err := svc.ListServices(serviceParams) + err := svc.ListServicesPages(serviceParams, func(page *ecs.ListServicesOutput, lastPage bool) bool { + for _, serviceArn := range page.ServiceArns { + resources = append(resources, &ECSService{ + svc: svc, + serviceARN: serviceArn, + clusterARN: clusterArn, + }) + } + return true + }) if err != nil { return nil, err } - - for _, serviceArn := range output.ServiceArns { - resources = append(resources, &ECSService{ - svc: svc, - serviceARN: serviceArn, - clusterARN: clusterArn, - }) - } - - if output.NextToken == nil { - continue - } - - serviceParams.NextToken = output.NextToken } return resources, nil diff --git a/resources/elasticache-replicationgroups.go b/resources/elasticache-replicationgroups.go index 8be22140..19d9aa90 100644 --- a/resources/elasticache-replicationgroups.go +++ b/resources/elasticache-replicationgroups.go @@ -40,8 +40,9 @@ func (l *ElasticacheReplicationGroupLister) List(_ context.Context, o interface{ for _, replicationGroup := range resp.ReplicationGroups { resources = append(resources, &ElasticacheReplicationGroup{ - svc: svc, - groupID: replicationGroup.ReplicationGroupId, + svc: svc, + groupID: replicationGroup.ReplicationGroupId, + createTime: replicationGroup.ReplicationGroupCreateTime, }) } diff --git a/resources/firehose-deliverystreams.go b/resources/firehose-deliverystreams.go index f3133aaf..19a483ae 100644 --- a/resources/firehose-deliverystreams.go +++ b/resources/firehose-deliverystreams.go @@ -44,10 +44,31 @@ func (l *FirehoseDeliveryStreamLister) List(_ context.Context, o interface{}) ([ } for _, deliveryStreamName := range output.DeliveryStreamNames { + tagParams := &firehose.ListTagsForDeliveryStreamInput{ + DeliveryStreamName: deliveryStreamName, + Limit: aws.Int64(50), + } + + for { + tagResp, tagErr := svc.ListTagsForDeliveryStream(tagParams) + if tagErr != nil { + return nil, tagErr + } + + tags = append(tags, tagResp.Tags...) + if !*tagResp.HasMoreTags { + break + } + + tagParams.ExclusiveStartTagKey = tagResp.Tags[len(tagResp.Tags)-1].Key + } + resources = append(resources, &FirehoseDeliveryStream{ svc: svc, deliveryStreamName: deliveryStreamName, + tags: tags, }) + lastDeliveryStreamName = deliveryStreamName } @@ -77,3 +98,13 @@ func (f *FirehoseDeliveryStream) Remove(_ context.Context) error { func (f *FirehoseDeliveryStream) String() string { return *f.deliveryStreamName } + +func (f *FirehoseDeliveryStream) Properties() types.Properties { + properties := types.NewProperties() + for _, tag := range f.tags { + properties.SetTag(tag.Key, tag.Value) + } + + properties.Set("Name", f.deliveryStreamName) + return properties +} diff --git a/resources/fms-policies.go b/resources/fms-policies.go index fdf6802e..77fadb61 100644 --- a/resources/fms-policies.go +++ b/resources/fms-policies.go @@ -38,6 +38,12 @@ func (l *FMSPolicyLister) List(_ context.Context, o interface{}) ([]resource.Res for { resp, err := svc.ListPolicies(params) if err != nil { + if aerr, ok := err.(awserr.Error); ok { + if strings.Contains(aerr.Message(), "No default admin could be found") { + logrus.Infof("FMSPolicy: %s. Ignore if you haven't set it up.", aerr.Message()) + return nil, nil + } + } return nil, err } diff --git a/resources/glue-securityconfigurations.go b/resources/glue-securityconfigurations.go new file mode 100644 index 00000000..bc03635f --- /dev/null +++ b/resources/glue-securityconfigurations.go @@ -0,0 +1,68 @@ +package resources + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/glue" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +type GlueSecurityConfiguration struct { + svc *glue.Glue + name *string +} + +func init() { + register("GlueSecurityConfiguration", ListGlueSecurityConfigurations) +} + +func ListGlueSecurityConfigurations(sess *session.Session) ([]Resource, error) { + svc := glue.New(sess) + resources := []Resource{} + + params := &glue.GetSecurityConfigurationsInput{ + MaxResults: aws.Int64(25), + } + + for { + output, err := svc.GetSecurityConfigurations(params) + if err != nil { + return nil, err + } + + for _, securityConfiguration := range output.SecurityConfigurations { + resources = append(resources, &GlueSecurityConfiguration{ + svc: svc, + name: securityConfiguration.Name, + }) + } + + // Check if there are more security configurations to fetch + if output.NextToken == nil || *output.NextToken == "" { + break + } + + params.NextToken = output.NextToken + } + + return resources, nil +} + +func (f *GlueSecurityConfiguration) Remove() error { + _, err := f.svc.DeleteSecurityConfiguration(&glue.DeleteSecurityConfigurationInput{ + Name: f.name, + }) + + return err +} + +func (f *GlueSecurityConfiguration) Properties() types.Properties { + properties := types.NewProperties() + properties.Set("Name", f.name) + + return properties +} + +func (f *GlueSecurityConfiguration) String() string { + return *f.name +} diff --git a/resources/iam-policies.go b/resources/iam-policies.go new file mode 100644 index 00000000..ece41af3 --- /dev/null +++ b/resources/iam-policies.go @@ -0,0 +1,117 @@ +package resources + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/iam" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" + "github.com/sirupsen/logrus" +) + +type IAMPolicy struct { + svc *iam.IAM + name string + policyId string + arn string + path string + tags []*iam.Tag +} + +func init() { + register("IAMPolicy", ListIAMPolicies) +} + +func GetIAMPolicy(svc *iam.IAM, policyArn *string) (*iam.Policy, error) { + params := &iam.GetPolicyInput{ + PolicyArn: policyArn, + } + resp, err := svc.GetPolicy(params) + return resp.Policy, err +} + +func ListIAMPolicies(sess *session.Session) ([]Resource, error) { + svc := iam.New(sess) + + params := &iam.ListPoliciesInput{ + Scope: aws.String("Local"), + } + + policies := make([]*iam.Policy, 0) + + err := svc.ListPoliciesPages(params, + func(page *iam.ListPoliciesOutput, lastPage bool) bool { + for _, listedPolicy := range page.Policies { + policy, err := GetIAMPolicy(svc, listedPolicy.Arn) + if err != nil { + logrus.Errorf("Failed to get listed policy %s: %v", *listedPolicy.PolicyName, err) + continue + } + policies = append(policies, policy) + } + return true + }) + if err != nil { + return nil, err + } + + resources := make([]Resource, 0) + + for _, out := range policies { + resources = append(resources, &IAMPolicy{ + svc: svc, + name: *out.PolicyName, + path: *out.Path, + arn: *out.Arn, + policyId: *out.PolicyId, + tags: out.Tags, + }) + } + + return resources, nil +} + +func (e *IAMPolicy) Remove() error { + resp, err := e.svc.ListPolicyVersions(&iam.ListPolicyVersionsInput{ + PolicyArn: &e.arn, + }) + if err != nil { + return err + } + for _, version := range resp.Versions { + if !*version.IsDefaultVersion { + _, err = e.svc.DeletePolicyVersion(&iam.DeletePolicyVersionInput{ + PolicyArn: &e.arn, + VersionId: version.VersionId, + }) + if err != nil { + return err + } + + } + } + _, err = e.svc.DeletePolicy(&iam.DeletePolicyInput{ + PolicyArn: &e.arn, + }) + if err != nil { + return err + } + + return nil +} + +func (policy *IAMPolicy) Properties() types.Properties { + properties := types.NewProperties() + + properties.Set("Name", policy.name) + properties.Set("ARN", policy.arn) + properties.Set("Path", policy.path) + properties.Set("PolicyID", policy.policyId) + for _, tag := range policy.tags { + properties.SetTag(tag.Key, tag.Value) + } + return properties +} + +func (e *IAMPolicy) String() string { + return e.arn +} diff --git a/resources/iam-users.go b/resources/iam-users.go new file mode 100644 index 00000000..7995fc0a --- /dev/null +++ b/resources/iam-users.go @@ -0,0 +1,91 @@ +package resources + +import ( + "time" + + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/iam" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" + "github.com/sirupsen/logrus" +) + +type IAMUser struct { + svc *iam.IAM + name string + tags []*iam.Tag + createDate *time.Time + passwordLastUsed *time.Time +} + +func init() { + register("IAMUser", ListIAMUsers) +} + +func GetIAMUser(svc *iam.IAM, userName *string) (*iam.User, error) { + params := &iam.GetUserInput{ + UserName: userName, + } + resp, err := svc.GetUser(params) + return resp.User, err +} + +func ListIAMUsers(sess *session.Session) ([]Resource, error) { + svc := iam.New(sess) + resources := []Resource{} + + err := svc.ListUsersPages(nil, func(page *iam.ListUsersOutput, lastPage bool) bool { + for _, out := range page.Users { + user, err := GetIAMUser(svc, out.UserName) + if err != nil { + logrus.Errorf("Failed to get user %s: %v", *out.UserName, err) + continue + } + resources = append(resources, &IAMUser{ + svc: svc, + name: *user.UserName, + tags: user.Tags, + createDate: user.CreateDate, + passwordLastUsed: user.PasswordLastUsed, + }) + } + return true + }) + if err != nil { + return nil, err + } + + return resources, nil +} + +func (e *IAMUser) Remove() error { + _, err := e.svc.DeleteUser(&iam.DeleteUserInput{ + UserName: &e.name, + }) + if err != nil { + return err + } + + return nil +} + +func (e *IAMUser) String() string { + return e.name +} + +func (e *IAMUser) Properties() types.Properties { + properties := types.NewProperties() + properties.Set("Name", e.name) + + if e.createDate != nil { + properties.Set("CreateDate", e.createDate.Format(time.RFC3339)) + } + if e.passwordLastUsed != nil { + properties.Set("PasswordLastUsed", e.passwordLastUsed.Format(time.RFC3339)) + } + + for _, tag := range e.tags { + properties.SetTag(tag.Key, tag.Value) + } + + return properties +} diff --git a/resources/kinesis-signaling-channels.go b/resources/kinesis-signaling-channels.go new file mode 100644 index 00000000..d1830f06 --- /dev/null +++ b/resources/kinesis-signaling-channels.go @@ -0,0 +1,60 @@ +package resources + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/kinesisvideo" +) + +type KinesisSignalingChannels struct { + svc *kinesisvideo.KinesisVideo + ChannelARN *string +} + +func init() { + register("KinesisSignalingChannels", ListKinesisSignalingChannels) +} + +func ListKinesisSignalingChannels(sess *session.Session) ([]Resource, error) { + svc := kinesisvideo.New(sess) + resources := []Resource{} + + params := &kinesisvideo.ListSignalingChannelsInput{ + MaxResults: aws.Int64(100), + } + + for { + output, err := svc.ListSignalingChannels(params) + if err != nil { + return nil, err + } + + for _, streamInfo := range output.ChannelInfoList { + resources = append(resources, &KinesisSignalingChannels{ + svc: svc, + ChannelARN: streamInfo.ChannelARN, + }) + } + + if output.NextToken == nil { + break + } + + params.NextToken = output.NextToken + } + + return resources, nil +} + +func (f *KinesisSignalingChannels) Remove() error { + + _, err := f.svc.DeleteSignalingChannel(&kinesisvideo.DeleteSignalingChannelInput{ + ChannelARN: f.ChannelARN, + }) + + return err +} + +func (f *KinesisSignalingChannels) String() string { + return *f.ChannelARN +} diff --git a/resources/kms-keys.go b/resources/kms-keys.go new file mode 100644 index 00000000..c7a67f68 --- /dev/null +++ b/resources/kms-keys.go @@ -0,0 +1,122 @@ +package resources + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/kms" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +type KMSKey struct { + svc *kms.KMS + id string + state string + manager *string + tags []*kms.Tag +} + +func init() { + register("KMSKey", ListKMSKeys) +} + +func ListKMSKeys(sess *session.Session) ([]Resource, error) { + svc := kms.New(sess) + resources := make([]Resource, 0) + + var innerErr error + err := svc.ListKeysPages(nil, func(resp *kms.ListKeysOutput, lastPage bool) bool { + for _, key := range resp.Keys { + resp, err := svc.DescribeKey(&kms.DescribeKeyInput{ + KeyId: key.KeyId, + }) + if err != nil { + innerErr = err + return false + } + + if *resp.KeyMetadata.KeyManager == kms.KeyManagerTypeAws { + continue + } + + if *resp.KeyMetadata.KeyState == kms.KeyStatePendingDeletion { + continue + } + + kmsKey := &KMSKey{ + svc: svc, + id: *resp.KeyMetadata.KeyId, + state: *resp.KeyMetadata.KeyState, + manager: resp.KeyMetadata.KeyManager, + } + + tags, err := svc.ListResourceTags(&kms.ListResourceTagsInput{ + KeyId: key.KeyId, + }) + if err != nil { + innerErr = err + return false + } + + kmsKey.tags = tags.Tags + resources = append(resources, kmsKey) + } + + if lastPage { + return false + } + + return true + }) + + if err != nil { + return nil, err + } + + if innerErr != nil { + return nil, err + } + + return resources, nil +} + +func (e *KMSKey) Filter() error { + if e.state == "PendingDeletion" { + return fmt.Errorf("is already in PendingDeletion state") + } + + if e.state == "PendingReplicaDeletion" { + return fmt.Errorf("is already in PendingReplicaDeletion state") + } + + if e.manager != nil && *e.manager == kms.KeyManagerTypeAws { + return fmt.Errorf("cannot delete AWS managed key") + } + + return nil +} + +func (e *KMSKey) Remove() error { + _, err := e.svc.ScheduleKeyDeletion(&kms.ScheduleKeyDeletionInput{ + KeyId: &e.id, + PendingWindowInDays: aws.Int64(7), + }) + return err +} + +func (e *KMSKey) String() string { + return e.id +} + +func (i *KMSKey) Properties() types.Properties { + properties := types.NewProperties() + properties. + Set("ID", i.id) + + for _, tag := range i.tags { + properties.SetTag(tag.TagKey, tag.TagValue) + } + + return properties +} diff --git a/resources/machinelearning-batchpredictions.go b/resources/machinelearning-batchpredictions.go index 0421d93f..83c23722 100644 --- a/resources/machinelearning-batchpredictions.go +++ b/resources/machinelearning-batchpredictions.go @@ -37,6 +37,12 @@ func (l *MachineLearningBranchPredictionLister) List(_ context.Context, o interf for { output, err := svc.DescribeBatchPredictions(params) if err != nil { + if aerr, ok := err.(awserr.Error); ok { + if strings.Contains(aerr.Message(), "AmazonML is no longer available to new customers") { + logrus.Info("MachineLearningBranchPrediction: AmazonML is no longer available to new customers. Ignore if you haven't set it up.") + return nil, nil + } + } return nil, err } diff --git a/resources/machinelearning-datasources.go b/resources/machinelearning-datasources.go index f4feb152..9e47df8e 100644 --- a/resources/machinelearning-datasources.go +++ b/resources/machinelearning-datasources.go @@ -37,6 +37,12 @@ func (l *MachineLearningDataSourceLister) List(_ context.Context, o interface{}) for { output, err := svc.DescribeDataSources(params) if err != nil { + if aerr, ok := err.(awserr.Error); ok { + if strings.Contains(aerr.Message(), "AmazonML is no longer available to new customers") { + logrus.Info("MachineLearningBranchPrediction: AmazonML is no longer available to new customers. Ignore if you haven't set it up.") + return nil, nil + } + } return nil, err } diff --git a/resources/machinelearning-evaluations.go b/resources/machinelearning-evaluations.go index f0c60298..decf3353 100644 --- a/resources/machinelearning-evaluations.go +++ b/resources/machinelearning-evaluations.go @@ -37,6 +37,12 @@ func (l *MachineLearningEvaluationLister) List(_ context.Context, o interface{}) for { output, err := svc.DescribeEvaluations(params) if err != nil { + if aerr, ok := err.(awserr.Error); ok { + if strings.Contains(aerr.Message(), "AmazonML is no longer available to new customers") { + logrus.Info("MachineLearningBranchPrediction: AmazonML is no longer available to new customers. Ignore if you haven't set it up.") + return nil, nil + } + } return nil, err } diff --git a/resources/machinelearning-mlmodels.go b/resources/machinelearning-mlmodels.go index 5e1e68e7..c3c2e4b2 100644 --- a/resources/machinelearning-mlmodels.go +++ b/resources/machinelearning-mlmodels.go @@ -37,6 +37,12 @@ func (l *MachineLearningMLModelLister) List(_ context.Context, o interface{}) ([ for { output, err := svc.DescribeMLModels(params) if err != nil { + if aerr, ok := err.(awserr.Error); ok { + if strings.Contains(aerr.Message(), "AmazonML is no longer available to new customers") { + logrus.Info("MachineLearningBranchPrediction: AmazonML is no longer available to new customers. Ignore if you haven't set it up.") + return nil, nil + } + } return nil, err } diff --git a/resources/polly-lexicons.go b/resources/polly-lexicons.go new file mode 100644 index 00000000..63889ab8 --- /dev/null +++ b/resources/polly-lexicons.go @@ -0,0 +1,57 @@ +package resources + +import ( + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/polly" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +type PollyLexicon struct { + svc *polly.Polly + name *string + attributes *polly.LexiconAttributes +} + +func init() { + register("PollyLexicons", ListPollyLexicons) +} + +func ListPollyLexicons(sess *session.Session) ([]Resource, error) { + svc := polly.New(sess) + resources := []Resource{} + + listLexiconsInput := &polly.ListLexiconsInput{} + + listOutput, err := svc.ListLexicons(listLexiconsInput) + if err != nil { + return nil, err + } + for _, lexicon := range listOutput.Lexicons { + resources = append(resources, &PollyLexicon{ + svc: svc, + name: lexicon.Name, + attributes: lexicon.Attributes, + }) + } + return resources, nil +} + +func (lexicon *PollyLexicon) Remove() error { + deleteInput := &polly.DeleteLexiconInput{ + Name: lexicon.name, + } + _, err := lexicon.svc.DeleteLexicon(deleteInput) + return err +} + +func (lexicon *PollyLexicon) Properties() types.Properties { + properties := types.NewProperties() + properties.Set("Name", lexicon.name) + properties.Set("Alphabet", lexicon.attributes.Alphabet) + properties.Set("LanguageCode", lexicon.attributes.LanguageCode) + properties.Set("LastModified", lexicon.attributes.LastModified) + properties.Set("LexemesCount", lexicon.attributes.LexemesCount) + properties.Set("LexiconArn", lexicon.attributes.LexiconArn) + properties.Set("Size", lexicon.attributes.Size) + return properties +} diff --git a/resources/route53-resolver-rules.go b/resources/route53-resolver-rules.go new file mode 100644 index 00000000..a274dc85 --- /dev/null +++ b/resources/route53-resolver-rules.go @@ -0,0 +1,140 @@ +package resources + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/route53resolver" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +type ( + // Route53ResolverRule is the resource type + Route53ResolverRule struct { + svc *route53resolver.Route53Resolver + id *string + name *string + domainName *string + vpcIds []*string + } +) + +func init() { + register("Route53ResolverRule", ListRoute53ResolverRules) +} + +// ListRoute53ResolverRules produces the resources to be nuked. +func ListRoute53ResolverRules(sess *session.Session) ([]Resource, error) { + svc := route53resolver.New(sess) + + vpcAssociations, err := resolverRulesToVpcIDs(svc) + if err != nil { + return nil, err + } + + var resources []Resource + + params := &route53resolver.ListResolverRulesInput{} + for { + resp, err := svc.ListResolverRules(params) + + if err != nil { + return nil, err + } + + for _, rule := range resp.ResolverRules { + resources = append(resources, &Route53ResolverRule{ + svc: svc, + id: rule.Id, + name: rule.Name, + domainName: rule.DomainName, + vpcIds: vpcAssociations[*rule.Id], + }) + } + + if resp.NextToken == nil { + break + } + + params.NextToken = resp.NextToken + } + + return resources, nil +} + +// Associate all the vpcIDs to their resolver rule ID to be disassociated before deleting the rule. +func resolverRulesToVpcIDs(svc *route53resolver.Route53Resolver) (map[string][]*string, error) { + vpcAssociations := map[string][]*string{} + + params := &route53resolver.ListResolverRuleAssociationsInput{} + + for { + resp, err := svc.ListResolverRuleAssociations(params) + + if err != nil { + return nil, err + } + + for _, ruleAssociation := range resp.ResolverRuleAssociations { + vpcID := ruleAssociation.VPCId + if vpcID != nil { + resolverRuleID := *ruleAssociation.ResolverRuleId + + if _, ok := vpcAssociations[resolverRuleID]; !ok { + vpcAssociations[resolverRuleID] = []*string{vpcID} + } else { + vpcAssociations[resolverRuleID] = append(vpcAssociations[resolverRuleID], vpcID) + } + } + } + + if resp.NextToken == nil { + break + } + + params.NextToken = resp.NextToken + } + + return vpcAssociations, nil +} + +// Filter removes resources automatically from being nuked +func (r *Route53ResolverRule) Filter() error { + if r.domainName != nil && *r.domainName == "." { + return fmt.Errorf(`Filtering DomainName "."`) + } + + return nil +} + +// Remove implements Resource +func (r *Route53ResolverRule) Remove() error { + for _, vpcID := range r.vpcIds { + _, err := r.svc.DisassociateResolverRule(&route53resolver.DisassociateResolverRuleInput{ + ResolverRuleId: r.id, + VPCId: vpcID, + }) + + if err != nil { + return err + } + } + + _, err := r.svc.DeleteResolverRule(&route53resolver.DeleteResolverRuleInput{ + ResolverRuleId: r.id, + }) + + return err +} + +// Properties provides debugging output +func (r *Route53ResolverRule) Properties() types.Properties { + return types.NewProperties(). + Set("ID", r.id). + Set("Name", r.name) +} + +// String implements Stringer +func (r *Route53ResolverRule) String() string { + return fmt.Sprintf("%s (%s)", *r.id, *r.name) +} diff --git a/resources/route53-resource-records.go b/resources/route53-resource-records.go index f450b723..7e89fec8 100644 --- a/resources/route53-resource-records.go +++ b/resources/route53-resource-records.go @@ -73,6 +73,7 @@ func ListResourceRecordsForZone(svc *route53.Route53, zoneID, zoneName *string) hostedZoneID: zoneID, hostedZoneName: zoneName, data: rrs, + tags: hostedZoneTags.ResourceTagSet.Tags, }) } @@ -132,9 +133,13 @@ func (r *Route53ResourceRecordSet) Remove(_ context.Context) error { } func (r *Route53ResourceRecordSet) Properties() types.Properties { - return types.NewProperties(). - Set("Name", r.data.Name). - Set("Type", r.data.Type) + properties := types.NewProperties() + for _, tag := range r.tags { + properties.SetTagWithPrefix("hz", tag.Key, tag.Value) + } + properties.Set("Name", r.data.Name) + properties.Set("Type", r.data.Type) + return properties } func (r *Route53ResourceRecordSet) String() string { diff --git a/resources/transcribe-call-analytics-categories.go b/resources/transcribe-call-analytics-categories.go new file mode 100644 index 00000000..892b27d3 --- /dev/null +++ b/resources/transcribe-call-analytics-categories.go @@ -0,0 +1,77 @@ +package resources + +import ( + "time" + + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/transcribeservice" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +type TranscribeCallAnalyticsCategory struct { + svc *transcribeservice.TranscribeService + name *string + inputType *string + createTime *time.Time + lastUpdateTime *time.Time +} + +func init() { + register("TranscribeCallAnalyticsCategory", ListTranscribeCallAnalyticsCategories) +} + +func ListTranscribeCallAnalyticsCategories(sess *session.Session) ([]Resource, error) { + svc := transcribeservice.New(sess) + resources := []Resource{} + var nextToken *string + + for { + listCallAnalyticsCategoriesInput := &transcribeservice.ListCallAnalyticsCategoriesInput{ + NextToken: nextToken, + } + + listOutput, err := svc.ListCallAnalyticsCategories(listCallAnalyticsCategoriesInput) + if err != nil { + return nil, err + } + for _, category := range listOutput.Categories { + resources = append(resources, &TranscribeCallAnalyticsCategory{ + svc: svc, + name: category.CategoryName, + inputType: category.InputType, + createTime: category.CreateTime, + lastUpdateTime: category.LastUpdateTime, + }) + } + + // Check if there are more results + if listOutput.NextToken == nil { + break // No more results, exit the loop + } + + // Set the nextToken for the next iteration + nextToken = listOutput.NextToken + } + return resources, nil +} + +func (category *TranscribeCallAnalyticsCategory) Remove() error { + deleteInput := &transcribeservice.DeleteCallAnalyticsCategoryInput{ + CategoryName: category.name, + } + _, err := category.svc.DeleteCallAnalyticsCategory(deleteInput) + return err +} + +func (category *TranscribeCallAnalyticsCategory) Properties() types.Properties { + properties := types.NewProperties() + properties.Set("Name", category.name) + properties.Set("InputType", category.inputType) + if category.createTime != nil { + properties.Set("CreateTime", category.createTime.Format(time.RFC3339)) + } + if category.lastUpdateTime != nil { + properties.Set("LastUpdateTime", category.lastUpdateTime.Format(time.RFC3339)) + } + return properties +} diff --git a/resources/transcribe-call-analytics-jobs.go b/resources/transcribe-call-analytics-jobs.go new file mode 100644 index 00000000..4971c9fb --- /dev/null +++ b/resources/transcribe-call-analytics-jobs.go @@ -0,0 +1,90 @@ +package resources + +import ( + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/transcribeservice" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +type TranscribeCallAnalyticsJob struct { + svc *transcribeservice.TranscribeService + name *string + status *string + completionTime *time.Time + creationTime *time.Time + failureReason *string + languageCode *string + startTime *time.Time +} + +func init() { + register("TranscribeCallAnalyticsJob", ListTranscribeCallAnalyticsJobs) +} + +func ListTranscribeCallAnalyticsJobs(sess *session.Session) ([]Resource, error) { + svc := transcribeservice.New(sess) + resources := []Resource{} + var nextToken *string + + for { + listCallAnalyticsJobsInput := &transcribeservice.ListCallAnalyticsJobsInput{ + MaxResults: aws.Int64(100), + NextToken: nextToken, + } + + listOutput, err := svc.ListCallAnalyticsJobs(listCallAnalyticsJobsInput) + if err != nil { + return nil, err + } + for _, job := range listOutput.CallAnalyticsJobSummaries { + resources = append(resources, &TranscribeCallAnalyticsJob{ + svc: svc, + name: job.CallAnalyticsJobName, + status: job.CallAnalyticsJobStatus, + completionTime: job.CompletionTime, + creationTime: job.CreationTime, + failureReason: job.FailureReason, + languageCode: job.LanguageCode, + startTime: job.StartTime, + }) + } + + // Check if there are more results + if listOutput.NextToken == nil { + break // No more results, exit the loop + } + + // Set the nextToken for the next iteration + nextToken = listOutput.NextToken + } + return resources, nil +} + +func (job *TranscribeCallAnalyticsJob) Remove() error { + deleteInput := &transcribeservice.DeleteCallAnalyticsJobInput{ + CallAnalyticsJobName: job.name, + } + _, err := job.svc.DeleteCallAnalyticsJob(deleteInput) + return err +} + +func (job *TranscribeCallAnalyticsJob) Properties() types.Properties { + properties := types.NewProperties() + properties.Set("Name", job.name) + properties.Set("Status", job.status) + if job.completionTime != nil { + properties.Set("CompletionTime", job.completionTime.Format(time.RFC3339)) + } + if job.creationTime != nil { + properties.Set("CreationTime", job.creationTime.Format(time.RFC3339)) + } + properties.Set("FailureReason", job.failureReason) + properties.Set("LanguageCode", job.languageCode) + if job.startTime != nil { + properties.Set("StartTime", job.startTime.Format(time.RFC3339)) + } + return properties +} diff --git a/resources/transcribe-language-models.go b/resources/transcribe-language-models.go new file mode 100644 index 00000000..aab986e8 --- /dev/null +++ b/resources/transcribe-language-models.go @@ -0,0 +1,91 @@ +package resources + +import ( + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/transcribeservice" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +type TranscribeLanguageModel struct { + svc *transcribeservice.TranscribeService + name *string + baseModelName *string + createTime *time.Time + failureReason *string + languageCode *string + lastModifiedTime *time.Time + modelStatus *string + upgradeAvailability *bool +} + +func init() { + register("TranscribeLanguageModel", ListTranscribeLanguageModels) +} + +func ListTranscribeLanguageModels(sess *session.Session) ([]Resource, error) { + svc := transcribeservice.New(sess) + resources := []Resource{} + var nextToken *string + + for { + listLanguageModelsInput := &transcribeservice.ListLanguageModelsInput{ + MaxResults: aws.Int64(100), + NextToken: nextToken, + } + + listOutput, err := svc.ListLanguageModels(listLanguageModelsInput) + if err != nil { + return nil, err + } + for _, model := range listOutput.Models { + resources = append(resources, &TranscribeLanguageModel{ + svc: svc, + name: model.ModelName, + baseModelName: model.BaseModelName, + createTime: model.CreateTime, + failureReason: model.FailureReason, + languageCode: model.LanguageCode, + lastModifiedTime: model.LastModifiedTime, + modelStatus: model.ModelStatus, + upgradeAvailability: model.UpgradeAvailability, + }) + } + + // Check if there are more results + if listOutput.NextToken == nil { + break // No more results, exit the loop + } + + // Set the nextToken for the next iteration + nextToken = listOutput.NextToken + } + return resources, nil +} + +func (model *TranscribeLanguageModel) Remove() error { + deleteInput := &transcribeservice.DeleteLanguageModelInput{ + ModelName: model.name, + } + _, err := model.svc.DeleteLanguageModel(deleteInput) + return err +} + +func (model *TranscribeLanguageModel) Properties() types.Properties { + properties := types.NewProperties() + properties.Set("Name", model.name) + properties.Set("BaseModelName", model.baseModelName) + if model.createTime != nil { + properties.Set("CreateTime", model.createTime.Format(time.RFC3339)) + } + properties.Set("FailureReason", model.failureReason) + properties.Set("LanguageCode", model.languageCode) + if model.lastModifiedTime != nil { + properties.Set("LastModifiedTime", model.lastModifiedTime.Format(time.RFC3339)) + } + properties.Set("ModelStatus", model.modelStatus) + properties.Set("UpgradeAvailability", model.upgradeAvailability) + return properties +} diff --git a/resources/transcribe-medical-transcription-jobs.go b/resources/transcribe-medical-transcription-jobs.go new file mode 100644 index 00000000..6547421c --- /dev/null +++ b/resources/transcribe-medical-transcription-jobs.go @@ -0,0 +1,102 @@ +package resources + +import ( + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/transcribeservice" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +type TranscribeMedicalTranscriptionJob struct { + svc *transcribeservice.TranscribeService + name *string + status *string + completionTime *time.Time + contentIdentificationType *string + creationTime *time.Time + failureReason *string + languageCode *string + outputLocationType *string + specialty *string + startTime *time.Time + inputType *string +} + +func init() { + register("TranscribeMedicalTranscriptionJob", ListTranscribeMedicalTranscriptionJobs) +} + +func ListTranscribeMedicalTranscriptionJobs(sess *session.Session) ([]Resource, error) { + svc := transcribeservice.New(sess) + resources := []Resource{} + var nextToken *string + + for { + listMedicalTranscriptionJobsInput := &transcribeservice.ListMedicalTranscriptionJobsInput{ + MaxResults: aws.Int64(100), + NextToken: nextToken, + } + + listOutput, err := svc.ListMedicalTranscriptionJobs(listMedicalTranscriptionJobsInput) + if err != nil { + return nil, err + } + for _, job := range listOutput.MedicalTranscriptionJobSummaries { + resources = append(resources, &TranscribeMedicalTranscriptionJob{ + svc: svc, + name: job.MedicalTranscriptionJobName, + status: job.TranscriptionJobStatus, + completionTime: job.CompletionTime, + contentIdentificationType: job.ContentIdentificationType, + creationTime: job.CreationTime, + failureReason: job.FailureReason, + languageCode: job.LanguageCode, + outputLocationType: job.OutputLocationType, + specialty: job.Specialty, + startTime: job.StartTime, + inputType: job.Type, + }) + } + + // Check if there are more results + if listOutput.NextToken == nil { + break // No more results, exit the loop + } + + // Set the nextToken for the next iteration + nextToken = listOutput.NextToken + } + return resources, nil +} + +func (job *TranscribeMedicalTranscriptionJob) Remove() error { + deleteInput := &transcribeservice.DeleteMedicalTranscriptionJobInput{ + MedicalTranscriptionJobName: job.name, + } + _, err := job.svc.DeleteMedicalTranscriptionJob(deleteInput) + return err +} + +func (job *TranscribeMedicalTranscriptionJob) Properties() types.Properties { + properties := types.NewProperties() + properties.Set("Name", job.name) + properties.Set("Status", job.status) + if job.completionTime != nil { + properties.Set("CompletionTime", job.completionTime.Format(time.RFC3339)) + } + properties.Set("ContentIdentificationType", job.contentIdentificationType) + if job.creationTime != nil { + properties.Set("CreationTime", job.creationTime.Format(time.RFC3339)) + } + properties.Set("FailureReason", job.failureReason) + properties.Set("LanguageCode", job.languageCode) + properties.Set("OutputLocationType", job.outputLocationType) + properties.Set("Specialty", job.specialty) + if job.startTime != nil { + properties.Set("StartTime", job.startTime.Format(time.RFC3339)) + } + properties.Set("InputType", job.inputType) + return properties +} diff --git a/resources/transcribe-medical-vocabularies.go b/resources/transcribe-medical-vocabularies.go new file mode 100644 index 00000000..0f4b1ffa --- /dev/null +++ b/resources/transcribe-medical-vocabularies.go @@ -0,0 +1,77 @@ +package resources + +import ( + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/transcribeservice" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +type TranscribeMedicalVocabulary struct { + svc *transcribeservice.TranscribeService + name *string + state *string + languageCode *string + lastModifiedTime *time.Time +} + +func init() { + register("TranscribeMedicalVocabulary", ListTranscribeMedicalVocabularies) +} + +func ListTranscribeMedicalVocabularies(sess *session.Session) ([]Resource, error) { + svc := transcribeservice.New(sess) + resources := []Resource{} + var nextToken *string + + for { + listMedicalVocabulariesInput := &transcribeservice.ListMedicalVocabulariesInput{ + MaxResults: aws.Int64(100), + NextToken: nextToken, + } + + listOutput, err := svc.ListMedicalVocabularies(listMedicalVocabulariesInput) + if err != nil { + return nil, err + } + for _, vocab := range listOutput.Vocabularies { + resources = append(resources, &TranscribeMedicalVocabulary{ + svc: svc, + name: vocab.VocabularyName, + state: vocab.VocabularyState, + languageCode: vocab.LanguageCode, + lastModifiedTime: vocab.LastModifiedTime, + }) + } + + // Check if there are more results + if listOutput.NextToken == nil { + break // No more results, exit the loop + } + + // Set the nextToken for the next iteration + nextToken = listOutput.NextToken + } + return resources, nil +} + +func (vocab *TranscribeMedicalVocabulary) Remove() error { + deleteInput := &transcribeservice.DeleteMedicalVocabularyInput{ + VocabularyName: vocab.name, + } + _, err := vocab.svc.DeleteMedicalVocabulary(deleteInput) + return err +} + +func (vocab *TranscribeMedicalVocabulary) Properties() types.Properties { + properties := types.NewProperties() + properties.Set("Name", vocab.name) + properties.Set("State", vocab.state) + properties.Set("LanguageCode", vocab.languageCode) + if vocab.lastModifiedTime != nil { + properties.Set("LastModifiedTime", vocab.lastModifiedTime.Format(time.RFC3339)) + } + return properties +} diff --git a/resources/transcribe-transcription-jobs.go b/resources/transcribe-transcription-jobs.go new file mode 100644 index 00000000..b4ff71f7 --- /dev/null +++ b/resources/transcribe-transcription-jobs.go @@ -0,0 +1,90 @@ +package resources + +import ( + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/transcribeservice" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +type TranscribeTranscriptionJob struct { + svc *transcribeservice.TranscribeService + name *string + status *string + completionTime *time.Time + creationTime *time.Time + failureReason *string + languageCode *string + startTime *time.Time +} + +func init() { + register("TranscribeTranscriptionJob", ListTranscribeTranscriptionJobs) +} + +func ListTranscribeTranscriptionJobs(sess *session.Session) ([]Resource, error) { + svc := transcribeservice.New(sess) + resources := []Resource{} + var nextToken *string + + for { + listTranscriptionJobsInput := &transcribeservice.ListTranscriptionJobsInput{ + MaxResults: aws.Int64(100), + NextToken: nextToken, + } + + listOutput, err := svc.ListTranscriptionJobs(listTranscriptionJobsInput) + if err != nil { + return nil, err + } + for _, job := range listOutput.TranscriptionJobSummaries { + resources = append(resources, &TranscribeTranscriptionJob{ + svc: svc, + name: job.TranscriptionJobName, + status: job.TranscriptionJobStatus, + completionTime: job.CompletionTime, + creationTime: job.CreationTime, + failureReason: job.FailureReason, + languageCode: job.LanguageCode, + startTime: job.StartTime, + }) + } + + // Check if there are more results + if listOutput.NextToken == nil { + break // No more results, exit the loop + } + + // Set the nextToken for the next iteration + nextToken = listOutput.NextToken + } + return resources, nil +} + +func (job *TranscribeTranscriptionJob) Remove() error { + deleteInput := &transcribeservice.DeleteTranscriptionJobInput{ + TranscriptionJobName: job.name, + } + _, err := job.svc.DeleteTranscriptionJob(deleteInput) + return err +} + +func (job *TranscribeTranscriptionJob) Properties() types.Properties { + properties := types.NewProperties() + properties.Set("Name", job.name) + properties.Set("Status", job.status) + if job.completionTime != nil { + properties.Set("CompletionTime", job.completionTime.Format(time.RFC3339)) + } + if job.creationTime != nil { + properties.Set("CreationTime", job.creationTime.Format(time.RFC3339)) + } + properties.Set("FailureReason", job.failureReason) + properties.Set("LanguageCode", job.languageCode) + if job.startTime != nil { + properties.Set("StartTime", job.startTime.Format(time.RFC3339)) + } + return properties +} diff --git a/resources/transcribe-vocabularies.go b/resources/transcribe-vocabularies.go new file mode 100644 index 00000000..18771b24 --- /dev/null +++ b/resources/transcribe-vocabularies.go @@ -0,0 +1,77 @@ +package resources + +import ( + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/transcribeservice" + "github.com/rebuy-de/aws-nuke/v2/pkg/types" +) + +type TranscribeVocabulary struct { + svc *transcribeservice.TranscribeService + name *string + state *string + languageCode *string + lastModifiedTime *time.Time +} + +func init() { + register("TranscribeVocabulary", ListTranscribeVocabularies) +} + +func ListTranscribeVocabularies(sess *session.Session) ([]Resource, error) { + svc := transcribeservice.New(sess) + resources := []Resource{} + var nextToken *string + + for { + listVocabulariesInput := &transcribeservice.ListVocabulariesInput{ + MaxResults: aws.Int64(100), + NextToken: nextToken, + } + + listOutput, err := svc.ListVocabularies(listVocabulariesInput) + if err != nil { + return nil, err + } + for _, vocab := range listOutput.Vocabularies { + resources = append(resources, &TranscribeVocabulary{ + svc: svc, + name: vocab.VocabularyName, + state: vocab.VocabularyState, + languageCode: vocab.LanguageCode, + lastModifiedTime: vocab.LastModifiedTime, + }) + } + + // Check if there are more results + if listOutput.NextToken == nil { + break // No more results, exit the loop + } + + // Set the nextToken for the next iteration + nextToken = listOutput.NextToken + } + return resources, nil +} + +func (vocab *TranscribeVocabulary) Remove() error { + deleteInput := &transcribeservice.DeleteVocabularyInput{ + VocabularyName: vocab.name, + } + _, err := vocab.svc.DeleteVocabulary(deleteInput) + return err +} + +func (vocab *TranscribeVocabulary) Properties() types.Properties { + properties := types.NewProperties() + properties.Set("Name", vocab.name) + properties.Set("State", vocab.state) + properties.Set("LanguageCode", vocab.languageCode) + if vocab.lastModifiedTime != nil { + properties.Set("LastModifiedTime", vocab.lastModifiedTime.Format(time.RFC3339)) + } + return properties +} diff --git a/resources/util.go b/resources/util.go new file mode 100644 index 00000000..b61b8396 --- /dev/null +++ b/resources/util.go @@ -0,0 +1,56 @@ +package resources + +import "github.com/aws/aws-sdk-go/aws/awserr" + +func UnPtrBool(ptr *bool, def bool) bool { + if ptr == nil { + return def + } + return *ptr +} + +func UnPtrString(ptr *string, def string) string { + if ptr == nil { + return def + } + return *ptr +} + +func EqualStringPtr(v1, v2 *string) bool { + if v1 == nil && v2 == nil { + return true + } + + if v1 == nil || v2 == nil { + return false + } + + return *v1 == *v2 +} + +func IsAWSError(err error, code string) bool { + aerr, ok := err.(awserr.Error) + if !ok { + return false + } + + return aerr.Code() == code +} + +func Chunk[T any](slice []T, size int) [][]T { + var chunks [][]T + for i := 0; i < len(slice); { + // Clamp the last chunk to the slice bound as necessary. + end := size + if l := len(slice[i:]); l < size { + end = l + } + + // Set the capacity of each chunk so that appending to a chunk does not + // modify the original slice. + chunks = append(chunks, slice[i:i+end:i+end]) + i += end + } + + return chunks +} diff --git a/resources/waf-rules.go b/resources/waf-rules.go index 47d98cd3..e9903246 100644 --- a/resources/waf-rules.go +++ b/resources/waf-rules.go @@ -42,9 +42,12 @@ func (l *WAFRuleLister) List(_ context.Context, o interface{}) ([]resource.Resou } for _, rule := range resp.Rules { - ruleResp, _ := svc.GetRule(&waf.GetRuleInput{ + ruleResp, err := svc.GetRule(&waf.GetRuleInput{ RuleId: rule.RuleId, }) + if err != nil { + return nil, err + } resources = append(resources, &WAFRule{ svc: svc, ID: rule.RuleId, diff --git a/resources/wafregional-rules.go b/resources/wafregional-rules.go index b1cb87a9..1de5c1b1 100644 --- a/resources/wafregional-rules.go +++ b/resources/wafregional-rules.go @@ -43,9 +43,12 @@ func (l *WAFRegionalRuleLister) List(_ context.Context, o interface{}) ([]resour } for _, rule := range resp.Rules { - ruleResp, _ := svc.GetRule(&waf.GetRuleInput{ + ruleResp, err := svc.GetRule(&waf.GetRuleInput{ RuleId: rule.RuleId, }) + if err != nil { + return nil, err + } resources = append(resources, &WAFRegionalRule{ svc: svc, ID: rule.RuleId, diff --git a/tools/list-cloudcontrol/main.go b/tools/list-cloudcontrol/main.go index f3de4e52..d007c7b2 100644 --- a/tools/list-cloudcontrol/main.go +++ b/tools/list-cloudcontrol/main.go @@ -37,67 +37,74 @@ func main() { mapping := registry.GetAlternativeResourceTypeMapping() in := &cloudformation.ListTypesInput{ - Type: aws.String(cloudformation.RegistryTypeResource), - Visibility: aws.String(cloudformation.VisibilityPublic), - ProvisioningType: aws.String(cloudformation.ProvisioningTypeFullyMutable), - } + Type: aws.String(cloudformation.RegistryTypeResource), + Visibility: aws.String(cloudformation.VisibilityPublic), - err = cf.ListTypesPagesWithContext(ctx, in, func(out *cloudformation.ListTypesOutput, _ bool) bool { - if out == nil { - return true - } - - for _, summary := range out.TypeSummaries { - if summary == nil { - continue - } - - typeName := aws.StringValue(summary.TypeName) - color.New(color.Bold).Printf("%-55s", typeName) - if !strings.HasPrefix(typeName, "AWS::") { - color.HiBlack("does not have a valid prefix") - continue - } - - describe, err := cf.DescribeType(&cloudformation.DescribeTypeInput{ - Type: aws.String(cloudformation.RegistryTypeResource), - TypeName: aws.String(typeName), - }) - if err != nil { - color.New(color.FgRed).Println(err) - continue - } - - var schema CFTypeSchema - err = json.Unmarshal([]byte(aws.StringValue(describe.Schema)), &schema) - if err != nil { - color.New(color.FgRed).Println(err) - continue - } + Filters: &cloudformation.TypeFilters{ + TypeNamePrefix: aws.String("AWS::"), + }, + } - _, canList := schema.Handlers["list"] - if !canList { - color.New(color.FgHiBlack).Println("does not support list") - continue + // Immutable objects don't have an `update` option, but can still be removed + for _, provisioningType := range []string{cloudformation.ProvisioningTypeFullyMutable, cloudformation.ProvisioningTypeImmutable} { + in.ProvisioningType = &provisioningType + err = cf.ListTypesPagesWithContext(ctx, in, func(out *cloudformation.ListTypesOutput, _ bool) bool { + if out == nil { + return true } - resourceName, exists := mapping[typeName] - if exists && resourceName == typeName { - fmt.Print("is only covered by ") - color.New(color.FgGreen, color.Bold).Println(resourceName) - continue - } else if exists { - fmt.Print("is also covered by ") - color.New(color.FgBlue, color.Bold).Println(resourceName) - continue + for _, summary := range out.TypeSummaries { + if summary == nil { + continue + } + + typeName := aws.StringValue(summary.TypeName) + color.New(color.Bold).Printf("%-55s", typeName) + if !strings.HasPrefix(typeName, "AWS::") { + color.HiBlack("does not have a valid prefix") + continue + } + + describe, err := cf.DescribeType(&cloudformation.DescribeTypeInput{ + Type: aws.String(cloudformation.RegistryTypeResource), + TypeName: aws.String(typeName), + }) + if err != nil { + color.New(color.FgRed).Println(err) + continue + } + + var schema CFTypeSchema + err = json.Unmarshal([]byte(aws.StringValue(describe.Schema)), &schema) + if err != nil { + color.New(color.FgRed).Println(err) + continue + } + + _, canList := schema.Handlers["list"] + if !canList { + color.New(color.FgHiBlack).Println("does not support list") + continue + } + + resourceName, exists := mapping[typeName] + if exists && resourceName == typeName { + fmt.Print("is only covered by ") + color.New(color.FgGreen, color.Bold).Println(resourceName) + continue + } else if exists { + fmt.Print("is also covered by ") + color.New(color.FgBlue, color.Bold).Println(resourceName) + continue + } + + color.New(color.FgYellow).Println("is not configured") } - color.New(color.FgYellow).Println("is not configured") + return true + }) + if err != nil { + logrus.Fatal(err) } - - return true - }) - if err != nil { - logrus.Fatal(err) } } diff --git a/tools/tools.go b/tools/tools.go new file mode 100644 index 00000000..356e8982 --- /dev/null +++ b/tools/tools.go @@ -0,0 +1,8 @@ +//go:build tools + +package main + +// https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module +import ( + _ "github.com/golang/mock/mockgen" +)