Repository: gruntwork-io/cloud-nuke
Pull request #673 (2024-04-05 00:56:59)

Refactor nuking default resources code.
<!-- Prepend '[WIP]' to the title if this PR is still a work-in-progress. Remove it when it is ready for review! -->

## Description

Partially Fixing https://github.com/gruntwork-io/cloud-nuke/issues/651
TODO: make sure to use default on all EC2 resources. 

<!-- Description of the changes introduced by this PR. -->

## TODOs

Read the [Gruntwork contribution guidelines](https://gruntwork.notion.site/Gruntwork-Coding-Methodology-02fdcd6e4b004e818553684760bf691e).

- [ ] Update the docs.
- [ ] Run the relevant tests successfully, including pre-commit checks.
- [ ] Ensure any 3rd party code adheres with our [license policy](https://www.notion.so/gruntwork/Gruntwork-licenses-and-open-source-usage-policy-f7dece1f780341c7b69c1763f22b1378) or delete this line if its not applicable.
- [ ] Include release notes. If this PR is backward incompatible, include a migration guide.
- [ ] Attention Grunts - if this PR adds support for a new resource, ensure the `nuke_sandbox` and `nuke_phxdevops` jobs in `.circleci/config.yml` have been updated with appropriate exclusions (either directly in the job or via the `.circleci/nuke_config.yml` file) to prevent nuking IAM roles, groups, resources, etc that are important for the test accounts.


## Release Notes (draft)

<!-- One-line description of the PR that can be included in the final release notes. -->
Added / Removed / Updated [X].

### Migration Guide

<!-- Important: If you made any backward incompatible changes, then you must write a migration guide! -->




Patch:
diff --git a/aws/aws.go b/aws/aws.go
index f46ea99..c54e23e 100644
--- a/aws/aws.go
+++ b/aws/aws.go
@@ -26,6 +26,11 @@ func GetAllResources(c context.Context, query *Query, configObj config.Config) (
 	configObj.AddTimeout(query.Timeout)
 
 	configObj.KMSCustomerKeys.IncludeUnaliasedKeys = query.ListUnaliasedKMSKeys
+
+	// Setting the DefaultOnly field
+	// This function only sets the objects that have the `DefaultOnly` field, currently VPC, Subnet, and Security Group.
+	configObj.AddEC2DefaultOnly(query.DefaultOnly)
+
 	account := AwsAccountResources{
 		Resources: make(map[string]AwsResources),
 	}
diff --git a/aws/query.go b/aws/query.go
index accbe91..680ebaf 100644
--- a/aws/query.go
+++ b/aws/query.go
@@ -14,10 +14,11 @@ type Query struct {
 	IncludeAfter         *time.Time
 	ListUnaliasedKMSKeys bool
 	Timeout              *time.Duration
+	DefaultOnly          bool
 }
 
 // NewQuery configures and returns a Query struct that can be passed into the InspectResources method
-func NewQuery(regions, excludeRegions, resourceTypes, excludeResourceTypes []string, excludeAfter, includeAfter *time.Time, listUnaliasedKMSKeys bool, timeout *time.Duration) (*Query, error) {
+func NewQuery(regions, excludeRegions, resourceTypes, excludeResourceTypes []string, excludeAfter, includeAfter *time.Time, listUnaliasedKMSKeys bool, timeout *time.Duration, defaultOnly bool) (*Query, error) {
 	q := &Query{
 		Regions:              regions,
 		ExcludeRegions:       excludeRegions,
@@ -27,6 +28,7 @@ func NewQuery(regions, excludeRegions, resourceTypes, excludeResourceTypes []str
 		IncludeAfter:         includeAfter,
 		ListUnaliasedKMSKeys: listUnaliasedKMSKeys,
 		Timeout:              timeout,
+		DefaultOnly:          defaultOnly,
 	}
 
 	validationErr := q.Validate()
diff --git a/aws/resources/ec2_subnet.go b/aws/resources/ec2_subnet.go
index 6cab275..bb75641 100644
--- a/aws/resources/ec2_subnet.go
+++ b/aws/resources/ec2_subnet.go
@@ -2,6 +2,7 @@ package resources
 
 import (
 	"context"
+	"strconv"
 	"time"
 
 	"github.com/aws/aws-sdk-go/aws"
@@ -63,7 +64,21 @@ func shouldIncludeEc2Subnet(subnet *ec2.Subnet, firstSeenTime *time.Time, config
 // Returns a formatted string of EC2 subnets
 func (ec2subnet *EC2Subnet) getAll(_ context.Context, configObj config.Config) ([]*string, error) {
 	result := []*string{}
-	err := ec2subnet.Client.DescribeSubnetsPages(&ec2.DescribeSubnetsInput{}, func(pages *ec2.DescribeSubnetsOutput, lastPage bool) bool {
+	// Note: This filter initially handles non-default resources and can be overridden by passing the only-default filter to choose default subnets.
+	if configObj.EC2Subnet.DefaultOnly {
+		logging.Debugf("[default only] Retrieving the default subnets")
+	}
+
+	err := ec2subnet.Client.DescribeSubnetsPages(&ec2.DescribeSubnetsInput{
+		Filters: []*ec2.Filter{
+			{
+				Name: aws.String("default-for-az"),
+				Values: []*string{
+					aws.String(strconv.FormatBool(configObj.EC2Subnet.DefaultOnly)), // convert the bool status into string
+				},
+			},
+		},
+	}, func(pages *ec2.DescribeSubnetsOutput, lastPage bool) bool {
 		for _, subnet := range pages.Subnets {
 
 			// check first seen tag
diff --git a/aws/resources/ec2_vpc.go b/aws/resources/ec2_vpc.go
index ac02902..03547b5 100644
--- a/aws/resources/ec2_vpc.go
+++ b/aws/resources/ec2_vpc.go
@@ -4,6 +4,8 @@ import (
 	"context"
 	cerrors "errors"
 	"fmt"
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"strconv"
 	"strings"
 	"time"
 
@@ -57,14 +59,15 @@ func (v *EC2VPCs) getFirstSeenTag(vpc ec2.Vpc) (*time.Time, error) {
 	return nil, nil
 }
 
-func (v *EC2VPCs) getAll(c context.Context, configObj config.Config) ([]*string, error) {
+func (v *EC2VPCs) getAll(_ context.Context, configObj config.Config) ([]*string, error) {
+	// Note: This filter initially handles non-default resources and can be overridden by passing the only-default filter to choose default VPCs.
 	result, err := v.Client.DescribeVpcs(&ec2.DescribeVpcsInput{
 		Filters: []*ec2.Filter{
-			// Note: this filter omits the default since there is special
-			// handling for default resources already
 			{
-				Name:   awsgo.String("is-default"),
-				Values: awsgo.StringSlice([]string{"false"}),
+				Name: awsgo.String("is-default"),
+				Values: []*string{
+					aws.String(strconv.FormatBool(configObj.VPC.DefaultOnly)), // convert the bool status into string
+				},
 			},
 		},
 	})
diff --git a/aws/resources/security_group.go b/aws/resources/security_group.go
index f640537..c20abb6 100644
--- a/aws/resources/security_group.go
+++ b/aws/resources/security_group.go
@@ -47,6 +47,12 @@ func (sg *SecurityGroup) getFirstSeenTag(securityGroup ec2.SecurityGroup) (*time
 // shouldIncludeSecurityGroup determines whether a security group should be included for deletion based on the provided configuration.
 func shouldIncludeSecurityGroup(sg *ec2.SecurityGroup, firstSeenTime *time.Time, configObj config.Config) bool {
 	var groupName = sg.GroupName
+
+	if !configObj.SecurityGroup.DefaultOnly && *groupName == "default" {
+		logging.Debugf("[default security group] skipping default security group including")
+		return false
+	}
+
 	return configObj.SecurityGroup.ShouldInclude(config.ResourceValue{
 		Name: groupName,
 		Tags: util.ConvertEC2TagsToMap(sg.Tags),
@@ -58,7 +64,24 @@ func shouldIncludeSecurityGroup(sg *ec2.SecurityGroup, firstSeenTime *time.Time,
 func (sg *SecurityGroup) getAll(_ context.Context, configObj config.Config) ([]*string, error) {
 	var identifiers []*string
 
-	resp, err := sg.Client.DescribeSecurityGroups(&ec2.DescribeSecurityGroupsInput{})
+	var filters []*ec2.Filter
+	if configObj.SecurityGroup.DefaultOnly {
+		// Note : we can't simply remove the default security groups. Instead, we're only able to revoke permissions on the security group rules.
+		// Setting a flag that can be accessed within the nuke method to check if the nuking is for default or not.
+		sg.NukeOnlyDefault = configObj.SecurityGroup.DefaultOnly
+
+		logging.Debugf("[default only] Retrieving the default security-groups")
+		filters = []*ec2.Filter{
+			{
+				Name:   aws.String("group-name"),
+				Values: aws.StringSlice([]string{"default"}),
+			},
+		}
+	}
+
+	resp, err := sg.Client.DescribeSecurityGroups(&ec2.DescribeSecurityGroupsInput{
+		Filters: filters,
+	})
 	if err != nil {
 		logging.Debugf("[Security Group] Failed to list security groups: %s", err)
 		return nil, errors.WithStackTrace(err)
@@ -106,12 +129,108 @@ func (sg *SecurityGroup) nuke(id *string) error {
 		return errors.WithStackTrace(err)
 	}
 
+	// check the nuking is only for default security groups, then nuke and return
+	if sg.NukeOnlyDefault {
+		// RevokeSecurityGroupIngress
+		if err := sg.RevokeSecurityGroupIngress(*id); err != nil {
+			return errors.WithStackTrace(err)
+		}
+
+		// RevokeSecurityGroupEgress
+		if err := sg.RevokeSecurityGroupEgress(*id); err != nil {
+			return errors.WithStackTrace(err)
+		}
+
+		// RevokeIPv6SecurityGroupEgress
+		if err := sg.RevokeIPv6SecurityGroupEgress(*id); err != nil {
+			return errors.WithStackTrace(err)
+		}
+
+		return nil
+	}
+
+	// nuke the securiy group which is not default one
 	if err := sg.nukeSecurityGroup(*id); err != nil {
 		return errors.WithStackTrace(err)
 	}
 	return nil
 }
 
+func (sg *SecurityGroup) RevokeSecurityGroupIngress(id string) error {
+	_, err := sg.Client.RevokeSecurityGroupIngress(&ec2.RevokeSecurityGroupIngressInput{
+		GroupId: aws.String(id),
+		IpPermissions: []*ec2.IpPermission{
+			{
+				IpProtocol:       aws.String("-1"),
+				FromPort:         aws.Int64(0),
+				ToPort:           aws.Int64(0),
+				UserIdGroupPairs: []*ec2.UserIdGroupPair{{GroupId: aws.String(id)}},
+			},
+		},
+	})
+	if err != nil {
+		if util.TransformAWSError(err) == util.ErrInvalidPermisionNotFound {
+			logging.Debugf("Ingress rule not present (ok)")
+			return nil
+		}
+
+		logging.Debugf("[Security Group] Failed to revoke security group ingress associated with security group %s: %s", id, err)
+		return errors.WithStackTrace(err)
+	}
+
+	return nil
+}
+
+func (sg *SecurityGroup) RevokeSecurityGroupEgress(id string) error {
+	_, err := sg.Client.RevokeSecurityGroupEgress(&ec2.RevokeSecurityGroupEgressInput{
+		GroupId: aws.String(id),
+		IpPermissions: []*ec2.IpPermission{
+			{
+				IpProtocol: aws.String("-1"),
+				FromPort:   aws.Int64(0),
+				ToPort:     aws.Int64(0),
+				IpRanges:   []*ec2.IpRange{{CidrIp: aws.String("0.0.0.0/0")}},
+			},
+		},
+	})
+	if err != nil {
+		if util.TransformAWSError(err) == util.ErrInvalidPermisionNotFound {
+			logging.Debugf("Egress rule not present (ok)")
+			return nil
+		}
+
+		logging.Debugf("[Security Group] Failed to revoke security group egress associated with security group %s: %s", id, err)
+		return errors.WithStackTrace(err)
+	}
+
+	return nil
+}
+
+func (sg *SecurityGroup) RevokeIPv6SecurityGroupEgress(id string) error {
+	_, err := sg.Client.RevokeSecurityGroupEgress(&ec2.RevokeSecurityGroupEgressInput{
+		GroupId: aws.String(id),
+		IpPermissions: []*ec2.IpPermission{
+			{
+				IpProtocol: aws.String("-1"),
+				FromPort:   aws.Int64(0),
+				ToPort:     aws.Int64(0),
+				Ipv6Ranges: []*ec2.Ipv6Range{{CidrIpv6: aws.String("::/0")}},
+			},
+		},
+	})
+	if err != nil {
+		if util.TransformAWSError(err) == util.ErrInvalidPermisionNotFound {
+			logging.Debugf("Ingress rule not present (ok)")
+			return nil
+		}
+
+		logging.Debugf("[Security Group] Failed to revoke security group egress associated with security group %s: %s", id, err)
+		return errors.WithStackTrace(err)
+	}
+
+	return nil
+}
+
 func (sg *SecurityGroup) terminateInstancesAssociatedWithSecurityGroup(id string) error {
 
 	resp, err := sg.Client.DescribeInstances(&ec2.DescribeInstancesInput{
diff --git a/aws/resources/security_group_types.go b/aws/resources/security_group_types.go
index 00b4995..7909824 100644
--- a/aws/resources/security_group_types.go
+++ b/aws/resources/security_group_types.go
@@ -13,9 +13,10 @@ import (
 
 type SecurityGroup struct {
 	BaseAwsResource
-	Client         ec2iface.EC2API
-	Region         string
-	SecurityGroups []string
+	Client          ec2iface.EC2API
+	Region          string
+	SecurityGroups  []string
+	NukeOnlyDefault bool
 }
 
 func (sg *SecurityGroup) Init(session *session.Session) {
diff --git a/commands/cli.go b/commands/cli.go
index 89d4042..3e26529 100644
--- a/commands/cli.go
+++ b/commands/cli.go
@@ -6,7 +6,6 @@ import (
 	"time"
 
 	"github.com/gruntwork-io/cloud-nuke/aws"
-	"github.com/gruntwork-io/cloud-nuke/aws/resources"
 
 	"github.com/gruntwork-io/cloud-nuke/telemetry"
 	commonTelemetry "github.com/gruntwork-io/go-commons/telemetry"
@@ -182,7 +181,7 @@ func CreateCli(version string) *cli.App {
 }
 
 func parseDurationParam(paramValue string) (*time.Time, error) {
-	if paramValue == "0s" {
+	if paramValue == "0s" || paramValue == "" {
 		return nil, nil
 	}
 
@@ -244,7 +243,65 @@ func awsNuke(c *cli.Context) error {
 		configObj = *configObjPtr
 	}
 
-	query, account, err := handleGetResources(c, configObj, c.Bool("delete-unaliased-kms-keys"))
+	query, err := generateQuery(c, c.Bool("delete-unaliased-kms-keys"), nil, false)
+	if err != nil {
+		return errors.WithStackTrace(err)
+	}
+
+	return awsNukeHelper(c, configObj, query)
+}
+
+func awsDefaults(c *cli.Context) error {
+	telemetry.TrackEvent(commonTelemetry.EventContext{
+		EventName: "Start aws-defaults",
+	}, map[string]interface{}{})
+	defer telemetry.TrackEvent(commonTelemetry.EventContext{
+		EventName: "End aws-defaults",
+	}, map[string]interface{}{})
+	parseErr := logging.ParseLogLevel(c.String("log-level"))
+	if parseErr != nil {
+		return errors.WithStackTrace(parseErr)
+	}
+
+	// Depending on the flag, we need to populate resource-types to include/exclude
+	resourceTypes := []string{"vpc"} // nuking the vpc will remove the attached default security groups
+	if c.Bool("sg-only") {
+		resourceTypes = []string{"security-group"}
+	}
+
+	query, err := generateQuery(c, false, resourceTypes, true)
+	if err != nil {
+		return errors.WithStackTrace(err)
+	}
+
+	// Note: config feature only available for awsNuke command.
+	return awsNukeHelper(c, config.Config{}, query)
+}
+
+func awsInspect(c *cli.Context) error {
+	telemetry.TrackEvent(commonTelemetry.EventContext{
+		EventName: "Start aws-inspect",
+	}, map[string]interface{}{})
+	defer telemetry.TrackEvent(commonTelemetry.EventContext{
+		EventName: "End aws-inspect",
+	}, map[string]interface{}{})
+
+	// Handle the case where the user only wants to list resource types
+	if c.Bool("list-resource-types") {
+		return handleListResourceTypes()
+	}
+
+	query, err := generateQuery(c, c.Bool("list-unaliased-kms-keys"), nil, false)
+	if err != nil {
+		return errors.WithStackTrace(err)
+	}
+
+	_, err = handleGetResources(c, config.Config{}, query)
+	return err
+}
+
+func awsNukeHelper(c *cli.Context, configObj config.Config, query *aws.Query) error {
+	account, err := handleGetResources(c, configObj, query)
 	if err != nil {
 		telemetry.TrackEvent(commonTelemetry.EventContext{
 			EventName: "Error getting resources",
@@ -305,259 +362,50 @@ func awsNuke(c *cli.Context) error {
 	}
 
 	ui.RenderRunReport()
-
 	return nil
 }
 
-func awsDefaults(c *cli.Context) error {
-	telemetry.TrackEvent(commonTelemetry.EventContext{
-		EventName: "Start aws-defaults",
-	}, map[string]interface{}{})
-	defer telemetry.TrackEvent(commonTelemetry.EventContext{
-		EventName: "End aws-defaults",
-	}, map[string]interface{}{})
-	parseErr := logging.ParseLogLevel(c.String("log-level"))
-	if parseErr != nil {
-		return errors.WithStackTrace(parseErr)
-	}
-
-	logging.Debug("Identifying enabled regions")
-	regions, err := aws.GetEnabledRegions()
-	if err != nil {
-		return errors.WithStackTrace(err)
-	}
-	for _, region := range regions {
-		logging.Debugf("Found enabled region %s", region)
-	}
-
-	selectedRegions := c.StringSlice("region")
-	excludedRegions := c.StringSlice("exclude-region")
-
-	// targetRegions uses selectedRegions and excludedRegions to create a final
-	// target region slice.
-	targetRegions, err := aws.GetTargetRegions(regions, selectedRegions, excludedRegions)
-	if err != nil {
-		telemetry.TrackEvent(commonTelemetry.EventContext{
-			EventName: "Error getting target regions",
-		}, map[string]interface{}{})
-		return fmt.Errorf("Failed to select regions: %s", err)
-	}
-
-	if c.Bool("sg-only") {
-		logging.Info("Not removing default VPCs.")
-	} else {
-		telemetry.TrackEvent(commonTelemetry.EventContext{
-			EventName: "Nuking default VPCs",
-		}, map[string]interface{}{})
-		err = nukeDefaultVpcs(c, targetRegions)
-		if err != nil {
-			telemetry.TrackEvent(commonTelemetry.EventContext{
-				EventName: "Error nuking default vpcs",
-			}, map[string]interface{}{})
-			return errors.WithStackTrace(err)
-		}
-	}
-	telemetry.TrackEvent(commonTelemetry.EventContext{
-		EventName: "Nuking default security groups",
-	}, map[string]interface{}{})
-	err = nukeDefaultSecurityGroups(c, targetRegions)
-	if err != nil {
-		telemetry.TrackEvent(commonTelemetry.EventContext{
-			EventName: "Error nuking default security groups",
-		}, map[string]interface{}{})
-		return errors.WithStackTrace(err)
-	}
-	ui.RenderRunReport()
-
-	return nil
-}
-
-func nukeDefaultVpcs(c *cli.Context, regions []string) error {
-	// Start a spinner so the user knows we're still performing work in the background
-	spinnerMsg := "Discovering default VPCs"
-
-	spinnerSuccess, spinnerErr := pterm.DefaultSpinner.
-		WithRemoveWhenDone(true).
-		Start(spinnerMsg)
-
-	if spinnerErr != nil {
-		return errors.WithStackTrace(spinnerErr)
-	}
-
-	vpcPerRegion := resources.NewVpcPerRegion(regions)
-	vpcPerRegion, err := resources.GetDefaultVpcs(vpcPerRegion)
-	if err != nil {
-		return errors.WithStackTrace(err)
-	}
-
-	// Stop the spinner
-	spinnerSuccess.Stop()
-
-	if len(vpcPerRegion) == 0 {
-		logging.Info("No default VPCs found.")
-		return nil
-	}
-
-	targetedRegionList := []pterm.BulletListItem{}
-
-	for _, vpc := range vpcPerRegion {
-		vpcDetailString := fmt.Sprintf("Default VPC %s %s", vpc.VpcId, vpc.Region)
-		targetedRegionList = append(targetedRegionList, pterm.BulletListItem{Level: 0, Text: vpcDetailString})
-	}
-
-	ui.WarningMessage("The following Default VPCs are targeted for destruction")
-
-	// Log which Default VPCs will be nuked
-	list := pterm.DefaultBulletList.
-		WithItems(targetedRegionList).
-		WithBullet(ui.TargetEmoji)
-
-	renderErr := list.Render()
-	if renderErr != nil {
-		return errors.WithStackTrace(renderErr)
-	}
-
-	var proceed bool
-	if !c.Bool("force") {
-		proceed, err = ui.RenderNukeConfirmationPrompt("Are you sure you want to nuke the default VPCs listed above? Enter 'nuke' to confirm (or exit with ^C)", 2)
-		if err != nil {
-			return err
-		}
-	}
-
-	if proceed || c.Bool("force") {
-		// Start nuke progress bar with correct number of items
-		progressBar, err := pterm.DefaultProgressbar.WithTotal(len(targetedRegionList)).Start()
-		if err != nil {
-			return errors.WithStackTrace(err)
-		}
-
-		err = resources.NukeVpcs(vpcPerRegion)
-		if err != nil {
-			logging.Errorf("[Failed] %s", err)
-		}
-
-		_, err = progressBar.Stop()
-		if err != nil {
-			return errors.WithStackTrace(err)
-		}
-	}
-	return nil
-}
-
-func nukeDefaultSecurityGroups(c *cli.Context, regions []string) error {
-	// Start a spinner so the user knows we're still performing work in the background
-	spinnerMsg := "Discovering default security groups"
-
-	spinnerSuccess, spinnerErr := pterm.DefaultSpinner.
-		WithRemoveWhenDone(true).
-		Start(spinnerMsg)
-
-	if spinnerErr != nil {
-		return errors.WithStackTrace(spinnerErr)
-	}
-
-	defaultSgs, err := resources.GetDefaultSecurityGroups(regions)
-	spinnerSuccess.Stop()
-	if err != nil {
-		return errors.WithStackTrace(err)
-	}
-
-	if len(defaultSgs) == 0 {
-		logging.Info("No default security groups found.")
-		return nil
-	}
-
-	targetedRegionList := []pterm.BulletListItem{}
-
-	for _, sg := range defaultSgs {
-		defaultSgDetailText := fmt.Sprintf("* Default rules for SG %s %s %s", sg.GroupId, sg.GroupName, sg.Region)
-		targetedRegionList = append(targetedRegionList, pterm.BulletListItem{Level: 0, Text: defaultSgDetailText})
-	}
-
-	ui.WarningMessage("The following Default security group rules are targeted for destruction")
-
-	// Log which default security group rules will be nuked
-	list := pterm.DefaultBulletList.
-		WithItems(targetedRegionList).
-		WithBullet(ui.TargetEmoji)
-
-	renderErr := list.Render()
-	if renderErr != nil {
-		return errors.WithStackTrace(renderErr)
-	}
-
-	var proceed bool
-	if !c.Bool("force") {
-		prompt := "\nAre you sure you want to nuke the rules in these default security groups ? Enter 'nuke' to confirm (or exit with ^C)"
-		proceed, err = ui.RenderNukeConfirmationPrompt(prompt, 2)
-		if err != nil {
-			return err
-		}
-	}
-
-	if proceed || c.Bool("force") {
-		err := resources.NukeDefaultSecurityGroupRules(defaultSgs)
-		if err != nil {
-			logging.Errorf("[Failed] %s", err)
-		}
-	}
-
-	return nil
-}
-
-func awsInspect(c *cli.Context) error {
-	telemetry.TrackEvent(commonTelemetry.EventContext{
-		EventName: "Start aws-inspect",
-	}, map[string]interface{}{})
-	defer telemetry.TrackEvent(commonTelemetry.EventContext{
-		EventName: "End aws-inspect",
-	}, map[string]interface{}{})
-
-	// Handle the case where the user only wants to list resource types
-	if c.Bool("list-resource-types") {
-		return handleListResourceTypes()
-	}
-
-	_, _, err := handleGetResources(c, config.Config{}, c.Bool("list-unaliased-kms-keys"))
-	return err
-}
-
-func handleGetResources(c *cli.Context, configObj config.Config, includeUnaliasedKmsKeys bool) (
-	*aws.Query, *aws.AwsAccountResources, error) {
+func generateQuery(c *cli.Context, includeUnaliasedKmsKeys bool, overridingResourceTypes []string, onlyDefault bool) (*aws.Query, error) {
 	excludeAfter, err := parseDurationParam(c.String("older-than"))
 	if err != nil {
-		return nil, nil, errors.WithStackTrace(err)
+		return nil, errors.WithStackTrace(err)
 	}
 
 	includeAfter, err := parseDurationParam(c.String("newer-than"))
 	if err != nil {
-		return nil, nil, errors.WithStackTrace(err)
+		return nil, errors.WithStackTrace(err)
 	}
 
 	timeout, err := parseTimeoutDurationParam(c.String("timeout"))
 	if err != nil {
-		return nil, nil, errors.WithStackTrace(err)
+		return nil, errors.WithStackTrace(err)
+	}
+
+	resourceTypes := c.StringSlice("resource-type")
+	if overridingResourceTypes != nil {
+		resourceTypes = overridingResourceTypes
 	}
 
-	query, err := aws.NewQuery(
+	return aws.NewQuery(
 		c.StringSlice("region"),
 		c.StringSlice("exclude-region"),
-		c.StringSlice("resource-type"),
+		resourceTypes,
 		c.StringSlice("exclude-resource-type"),
 		excludeAfter,
 		includeAfter,
 		includeUnaliasedKmsKeys,
 		timeout,
+		onlyDefault,
 	)
-	if err != nil {
-		return nil, nil, aws.QueryCreationError{Underlying: err}
-	}
+}
+
+func handleGetResources(c *cli.Context, configObj config.Config, query *aws.Query) (
+	*aws.AwsAccountResources, error) {
 
 	pterm.DefaultSection.WithTopPadding(1).WithBottomPadding(0).Println("AWS Resource Query Parameters")
-	err = ui.RenderQueryAsBulletList(query)
+	err := ui.RenderQueryAsBulletList(query)
 	if err != nil {
-		return nil, nil, errors.WithStackTrace(err)
+		return nil, errors.WithStackTrace(err)
 	}
 	pterm.Println()
 
@@ -566,13 +414,13 @@ func handleGetResources(c *cli.Context, configObj config.Config, includeUnaliase
 		telemetry.TrackEvent(commonTelemetry.EventContext{
 			EventName: "Error inspecting resources",
 		}, map[string]interface{}{})
-		return nil, nil, errors.WithStackTrace(aws.ResourceInspectionError{Underlying: err})
+		return nil, errors.WithStackTrace(aws.ResourceInspectionError{Underlying: err})
 	}
 
 	pterm.DefaultSection.WithTopPadding(1).WithBottomPadding(0).Println("Found AWS Resources")
 	err = ui.RenderResourcesAsTable(accountResources)
 
-	return query, accountResources, err
+	return accountResources, err
 }
 
 func handleListResourceTypes() error {
diff --git a/config/config.go b/config/config.go
index 0434aaf..b8fc3c5 100644
--- a/config/config.go
+++ b/config/config.go
@@ -45,7 +45,7 @@ type Config struct {
 	EC2IPAMResourceDiscovery        ResourceType               `yaml:"EC2IPAMResourceDiscovery"`
 	EC2IPAMScope                    ResourceType               `yaml:"EC2IPAMScope"`
 	EC2Endpoint                     ResourceType               `yaml:"EC2Endpoint"`
-	EC2Subnet                       ResourceType               `yaml:"EC2Subnet"`
+	EC2Subnet                       EC2ResourceType            `yaml:"EC2Subnet"`
 	EgressOnlyInternetGateway       ResourceType               `yaml:"EgressOnlyInternetGateway"`
 	ECRRepository                   ResourceType               `yaml:"ECRRepository"`
 	ECSCluster                      ResourceType               `yaml:"ECSCluster"`
@@ -97,14 +97,14 @@ type Config struct {
 	TransitGatewayRouteTable        ResourceType               `yaml:"TransitGatewayRouteTable"`
 	TransitGatewaysVpcAttachment    ResourceType               `yaml:"TransitGatewaysVpcAttachment"`
 	TransitGatewayPeeringAttachment ResourceType               `yaml:"TransitGatewayPeeringAttachment"`
-	VPC                             ResourceType               `yaml:"VPC"`
+	VPC                             EC2ResourceType            `yaml:"VPC"`
 	Route53HostedZone               ResourceType               `yaml:"Route53HostedZone"`
 	Route53CIDRCollection           ResourceType               `yaml:"Route53CIDRCollection"`
 	Route53TrafficPolicy            ResourceType               `yaml:"Route53TrafficPolicy"`
 	InternetGateway                 ResourceType               `yaml:"InternetGateway"`
 	NetworkACL                      ResourceType               `yaml:"NetworkACL"`
-	SecurityGroup                   ResourceType               `yaml:"SecurityGroup"`
 	NetworkInterface                ResourceType               `yaml:"NetworkInterface"`
+	SecurityGroup                   EC2ResourceType            `yaml:"SecurityGroup"`
 }
 
 func (c *Config) addTimeAfterFilter(timeFilter *time.Time, fieldName string) {
@@ -144,6 +144,30 @@ func (c *Config) addTimeOut(timeout *time.Duration, fieldName string) {
 	}
 }
 
+func (c *Config) addDefautlOnly(flag bool) {
+	// Do nothing if the flag filter is false, by default it will be false
+	if flag == false {
+		return
+	}
+
+	v := reflect.ValueOf(c).Elem()
+	for i := 0; i < v.NumField(); i++ {
+		field := v.Field(i)
+		if field.Kind() != reflect.Struct {
+			continue
+		}
+
+		defaultOnlyField := field.FieldByName("DefaultOnly")
+		// IsValid reports whether v represents a value.
+		// It returns false if v is the zero Value.
+		// If IsValid returns false, all other methods except String panic.
+		if defaultOnlyField.IsValid() {
+			defaultOnlyVal := defaultOnlyField.Addr().Interface().(*bool)
+			*defaultOnlyVal = flag
+		}
+	}
+}
+
 func (c *Config) AddIncludeAfterTime(includeAfter *time.Time) {
 	// include after filter has been applied to all resources via `newer-than` flag, we are
 	// setting this rule across all resource types.
@@ -157,15 +181,25 @@ func (c *Config) AddExcludeAfterTime(excludeAfter *time.Time) {
 }
 
 func (c *Config) AddTimeout(timeout *time.Duration) {
-	// include after filter has been applied to all resources via `newer-than` flag, we are
+	// timeout filter has been applied to all resources via `timeout` flag, we are
 	// setting this rule across all resource types.
 	c.addTimeOut(timeout, "Timeout")
 }
 
+func (c *Config) AddEC2DefaultOnly(flag bool) {
+	// The flag filter has been applied to all resources via the default-only flag.
+	// We are now setting this rule across all resource types that have a field named `DefaultOnly`.
+	c.addDefautlOnly(flag)
+}
+
 type KMSCustomerKeyResourceType struct {
 	IncludeUnaliasedKeys bool `yaml:"include_unaliased_keys"`
 	ResourceType         `yaml:",inline"`
 }
+type EC2ResourceType struct {
+	DefaultOnly  bool `yaml:"default_only"`
+	ResourceType `yaml:",inline"`
+}
 
 type ResourceType struct {
 	IncludeRule FilterRule `yaml:"include"`
diff --git a/util/error.go b/util/error.go
index 76e78b0..d3b26a4 100644
--- a/util/error.go
+++ b/util/error.go
@@ -12,6 +12,7 @@ var ErrInSufficientPermission = errors.New("error:INSUFFICIENT_PERMISSION")
 var ErrDifferentOwner = errors.New("error:DIFFERENT_OWNER")
 var ErrContextExecutionTimeout = errors.New("error:EXECUTION_TIMEOUT")
 var ErrInterfaceIDNotFound = errors.New("error:InterfaceIdNotFound")
+var ErrInvalidPermisionNotFound = errors.New("error:InvalidPermission.NotFound")
 
 const AWsUnauthorizedError string = "UnauthorizedOperation"
 const AwsDryRunSuccess string = "Request would have succeeded, but DryRun flag is set."
@@ -36,6 +37,9 @@ func TransformAWSError(err error) error {
 	if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "DryRunOperation" && awsErr.Message() == AwsDryRunSuccess {
 		return nil
 	}
+	if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "InvalidPermission.NotFound" {
+		return ErrInvalidPermisionNotFound
+	}
 	return err
 
 }
diff --git a/util/time.go b/util/time.go
index bf1268c..bffa5df 100644
--- a/util/time.go
+++ b/util/time.go
@@ -1,10 +1,11 @@
 package util
 
 import (
+	"time"
+
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/gruntwork-io/cloud-nuke/logging"
 	"github.com/gruntwork-io/go-commons/errors"
-	"time"
 )
 
 const (
@@ -16,7 +17,8 @@ const (
 	FirstSeenTagKey = "cloud-nuke-first-seen"
 
 	// The time format of the `firstSeenTagKey` tag value.
-	firstSeenTimeFormat = time.RFC3339
+	firstSeenTimeFormat       = time.RFC3339
+	firstSeenTimeFormatLegacy = time.DateTime
 )
 
 func IsFirstSeenTag(key *string) bool {
@@ -26,9 +28,12 @@ func IsFirstSeenTag(key *string) bool {
 func ParseTimestamp(timestamp *string) (*time.Time, error) {
 	parsed, err := time.Parse(firstSeenTimeFormat, aws.StringValue(timestamp))
 	if err != nil {
-		logging.Debugf("Error parsing the timestamp into a `RFC3339` Time format")
-		return nil, errors.WithStackTrace(err)
-
+		logging.Debugf("Error parsing the timestamp into a `RFC3339` Time format. Trying parsing the timestamp using the legacy `time.DateTime` format.")
+		parsed, err = time.Parse(firstSeenTimeFormatLegacy, aws.StringValue(timestamp))
+		if err != nil {
+			logging.Debugf("Error parsing the timestamp into legacy `time.DateTime` Time format")
+			return nil, errors.WithStackTrace(err)
+		}
 	}
 
 	return &parsed, nil


Test patch:
diff --git a/aws/resources/ec2_subnet_test.go b/aws/resources/ec2_subnet_test.go
index 81101ed..eebd547 100644
--- a/aws/resources/ec2_subnet_test.go
+++ b/aws/resources/ec2_subnet_test.go
@@ -76,27 +76,32 @@ func TestEc2Subnets_GetAll(t *testing.T) {
 	ec2subnet.BaseAwsResource.Init(nil)
 
 	tests := map[string]struct {
-		configObj config.ResourceType
+		configObj config.EC2ResourceType
 		expected  []string
 	}{
 		"emptyFilter": {
-			configObj: config.ResourceType{},
+			configObj: config.EC2ResourceType{},
 			expected:  []string{subnet1, subnet2},
 		},
 		"nameExclusionFilter": {
-			configObj: config.ResourceType{
-				ExcludeRule: config.FilterRule{
-					NamesRegExp: []config.Expression{{
-						RE: *regexp.MustCompile(testName1),
-					}}},
+			configObj: config.EC2ResourceType{
+				ResourceType: config.ResourceType{
+					ExcludeRule: config.FilterRule{
+						NamesRegExp: []config.Expression{{
+							RE: *regexp.MustCompile(testName1),
+						}},
+					},
+				},
 			},
 			expected: []string{subnet2},
 		},
 		"timeAfterExclusionFilter": {
-			configObj: config.ResourceType{
-				ExcludeRule: config.FilterRule{
-					TimeAfter: aws.Time(now.Add(-1 * time.Hour)),
-				}},
+			configObj: config.EC2ResourceType{
+				ResourceType: config.ResourceType{
+					ExcludeRule: config.FilterRule{
+						TimeAfter: aws.Time(now.Add(-1 * time.Hour)),
+					}},
+			},
 			expected: []string{},
 		},
 	}
diff --git a/aws/resources/ec2_vpc_test.go b/aws/resources/ec2_vpc_test.go
index 0e4ee10..48b69f1 100644
--- a/aws/resources/ec2_vpc_test.go
+++ b/aws/resources/ec2_vpc_test.go
@@ -2,6 +2,10 @@ package resources
 
 import (
 	"context"
+	"regexp"
+	"testing"
+	"time"
+
 	"github.com/aws/aws-sdk-go-v2/aws"
 	awsgo "github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/service/ec2"
@@ -10,9 +14,6 @@ import (
 	"github.com/gruntwork-io/cloud-nuke/telemetry"
 	"github.com/gruntwork-io/cloud-nuke/util"
 	"github.com/stretchr/testify/require"
-	"regexp"
-	"testing"
-	"time"
 )
 
 type mockedEC2VPCs struct {
@@ -74,27 +75,32 @@ func TestEC2VPC_GetAll(t *testing.T) {
 	}
 
 	tests := map[string]struct {
-		configObj config.ResourceType
+		configObj config.EC2ResourceType
 		expected  []string
 	}{
 		"emptyFilter": {
-			configObj: config.ResourceType{},
+			configObj: config.EC2ResourceType{},
 			expected:  []string{testId1, testId2},
 		},
 		"nameExclusionFilter": {
-			configObj: config.ResourceType{
-				ExcludeRule: config.FilterRule{
-					NamesRegExp: []config.Expression{{
-						RE: *regexp.MustCompile(testName1),
-					}}},
+			configObj: config.EC2ResourceType{
+				ResourceType: config.ResourceType{
+					ExcludeRule: config.FilterRule{
+						NamesRegExp: []config.Expression{{
+							RE: *regexp.MustCompile(testName1),
+						}},
+					},
+				},
 			},
 			expected: []string{testId2},
 		},
 		"timeAfterExclusionFilter": {
-			configObj: config.ResourceType{
-				ExcludeRule: config.FilterRule{
-					TimeAfter: aws.Time(now.Add(-1 * time.Hour)),
-				}},
+			configObj: config.EC2ResourceType{
+				ResourceType: config.ResourceType{
+					ExcludeRule: config.FilterRule{
+						TimeAfter: aws.Time(now.Add(-1 * time.Hour)),
+					}},
+			},
 			expected: []string{},
 		},
 	}
diff --git a/aws/resources/security_group_test.go b/aws/resources/security_group_test.go
index 85e8136..2e9d5cb 100644
--- a/aws/resources/security_group_test.go
+++ b/aws/resources/security_group_test.go
@@ -89,27 +89,31 @@ func TestSecurityGroup_GetAll(t *testing.T) {
 	}
 
 	tests := map[string]struct {
-		configObj config.ResourceType
+		configObj config.EC2ResourceType
 		expected  []string
 	}{
 		"emptyFilter": {
-			configObj: config.ResourceType{},
+			configObj: config.EC2ResourceType{},
 			expected:  []string{testId1, testId2},
 		},
 		"nameExclusionFilter": {
-			configObj: config.ResourceType{
-				ExcludeRule: config.FilterRule{
-					NamesRegExp: []config.Expression{{
-						RE: *regexp.MustCompile(testName1),
-					}}},
+			configObj: config.EC2ResourceType{
+				ResourceType: config.ResourceType{
+					ExcludeRule: config.FilterRule{
+						NamesRegExp: []config.Expression{{
+							RE: *regexp.MustCompile(testName1),
+						}}},
+				},
 			},
 			expected: []string{testId2},
 		},
 		"timeAfterExclusionFilter": {
-			configObj: config.ResourceType{
-				ExcludeRule: config.FilterRule{
-					TimeAfter: aws.Time(now),
-				}},
+			configObj: config.EC2ResourceType{
+				ResourceType: config.ResourceType{
+					ExcludeRule: config.FilterRule{
+						TimeAfter: aws.Time(now),
+					}},
+				},
 			expected: []string{testId1},
 		},
 	}
diff --git a/config/config_test.go b/config/config_test.go
index 53e9ec9..2b57bdb 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -43,7 +43,7 @@ func emptyConfig() *Config {
 		ResourceType{FilterRule{}, FilterRule{}, ""},
 		ResourceType{FilterRule{}, FilterRule{}, ""},
 		ResourceType{FilterRule{}, FilterRule{}, ""},
-		ResourceType{FilterRule{}, FilterRule{}, ""},
+		EC2ResourceType{false, ResourceType{FilterRule{}, FilterRule{}, ""}},
 		ResourceType{FilterRule{}, FilterRule{}, ""},
 		ResourceType{FilterRule{}, FilterRule{}, ""},
 		ResourceType{FilterRule{}, FilterRule{}, ""},
@@ -95,14 +95,14 @@ func emptyConfig() *Config {
 		ResourceType{FilterRule{}, FilterRule{}, ""},
 		ResourceType{FilterRule{}, FilterRule{}, ""},
 		ResourceType{FilterRule{}, FilterRule{}, ""},
+		EC2ResourceType{false, ResourceType{FilterRule{}, FilterRule{}, ""}},
 		ResourceType{FilterRule{}, FilterRule{}, ""},
 		ResourceType{FilterRule{}, FilterRule{}, ""},
 		ResourceType{FilterRule{}, FilterRule{}, ""},
 		ResourceType{FilterRule{}, FilterRule{}, ""},
 		ResourceType{FilterRule{}, FilterRule{}, ""},
 		ResourceType{FilterRule{}, FilterRule{}, ""},
-		ResourceType{FilterRule{}, FilterRule{}, ""},
-		ResourceType{FilterRule{}, FilterRule{}, ""},
+		EC2ResourceType{false, ResourceType{FilterRule{}, FilterRule{}, ""}},
 	}
 }
 
diff --git a/util/time_test.go b/util/time_test.go
new file mode 100644
index 0000000..a9e810f
--- /dev/null
+++ b/util/time_test.go
@@ -0,0 +1,50 @@
+package util
+
+import (
+	"reflect"
+	"testing"
+	"time"
+
+	"github.com/aws/aws-sdk-go/aws"
+)
+
+func TestParseTimestamp(t *testing.T) {
+	type args struct {
+		timestamp *string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    *time.Time
+		wantErr bool
+	}{
+		{
+			"it should parse legacy firstSeenTag value \"2023-12-19 10:38:44\" correctly",
+			args{timestamp: aws.String("2023-12-19 10:38:44")},
+			newTime(time.Date(2023, 12, 19, 10, 38, 44, 0, time.UTC)),
+			false,
+		},
+		{
+			"it should parse RFC3339 firstSeenTag value \"2024-04-12T15:18:05Z\" correctly",
+			args{timestamp: aws.String("2024-04-12T15:18:05Z")},
+			newTime(time.Date(2024, 4, 12, 15, 18, 5, 0, time.UTC)),
+			false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := ParseTimestamp(tt.args.timestamp)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("ParseTimestamp() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("ParseTimestamp() = %v, want %v", got, &tt.want)
+			}
+		})
+	}
+}
+
+func newTime(t time.Time) *time.Time {
+	return &t
+}