Repository: gruntwork-io/cloud-nuke
Pull request #668 (2024-04-03 05:30:32)

Added winget install instructions to readme
## Description

Added winget install instructions to readme

## TODOs

- [x] Update the docs.
- [x] Wait for PR https://github.com/microsoft/winget-pkgs/pull/147448 to be merged
- [x] Wait for cloud-nuke to appear in winget index

![image](https://github.com/gruntwork-io/cloud-nuke/assets/32421608/a7dc548c-7556-47f8-b881-6f9c4859ca89)


## Release Notes (draft)

Updated Readme




Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index faa526f..9dc2f06 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -61,6 +61,7 @@ jobs:
               --exclude-resource-type config-rules \
               --exclude-resource-type eip \
               --exclude-resource-type nat-gateway \
+              --exclude-resource-type ec2-subnet \
               --delete-unaliased-kms-keys \
               --log-level debug
           no_output_timeout: 1h
@@ -85,6 +86,7 @@ jobs:
               --exclude-resource-type config-rules \
               --exclude-resource-type eip \
               --exclude-resource-type nat-gateway \
+              --exclude-resource-type ec2-subnet \
               --delete-unaliased-kms-keys \
               --log-level debug
           no_output_timeout: 1h
diff --git a/README.md b/README.md
index 562b85a..6a2bd8c 100644
--- a/README.md
+++ b/README.md
@@ -35,6 +35,8 @@ Cloud-nuke suppports 🔎 inspecting and 🔥💀 deleting the following AWS res
 | EC2                     | Internet Gateway        							                   |
 | EC2                     | Egress only internet gateway					                   |
 | EC2                     | Endpoint                							                   |
+| EC2                     | Security Group           							                   |
+| EC2                     | Network Interface        							                   |
 | Certificate Manager     | ACM Private CA                                           |
 | Direct Connect          | Transit Gateways                                         |
 | Elasticache             | Clusters                                                 |
@@ -160,6 +162,8 @@ to [download from the releases page](https://github.com/gruntwork-io/cloud-nuke/
 
 - **Linux:** Most Linux users can use [Homebrew](https://docs.brew.sh/Homebrew-on-Linux): `brew install cloud-nuke`.
 
+- **Windows:** You can install cloud-nuke using [winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/): `winget install cloud-nuke`
+
 ## Usage
 
 Simply running `cloud-nuke aws` will start the process of cleaning up your cloud account. You'll be shown a list of
@@ -580,6 +584,7 @@ of the file that are supported are listed here.
 | macie-member                | MacieMember                  | ❌                                     | ✅ (Creation Time)                   | ❌    |    ❌   |
 | msk-cluster                 | MSKCluster                   | ✅ (Cluster Name)                      | ✅ (Creation Time)                   | ❌    |    ❌   |
 | nat-gateway                 | NatGateway                   | ✅ (EC2 Name Tag)                      | ✅ (Creation Time)                   | ✅    |    ❌   |
+| network-interface           | NetworkInterface             | ✅ (Interface Name Tag)                | ✅ (Creation Time)                   | ✅    |    ❌   |
 | oidcprovider                | OIDCProvider                 | ✅ (Provider URL)                      | ✅ (Creation Time)                   | ❌    |    ❌   |
 | opensearchdomain            | OpenSearchDomain             | ✅ (Domain Name)                       | ✅ (First Seen Tag Time)             | ❌    |    ❌   |
 | redshift                    | Redshift                     | ✅ (Cluster Identifier)                | ✅ (Creation Time)                   | ❌    |    ❌   |
@@ -591,6 +596,7 @@ of the file that are supported are listed here.
 | s3-ap                       | s3AccessPoint                | ✅ (Access point Name)                 | ❌                                   | ❌    |    ❌   |
 | s3-olap                     | S3ObjectLambdaAccessPoint    | ✅ (Object Lambda Access point Name)   | ❌                                   | ❌    |    ❌   |
 | s3-mrap                     | S3MultiRegionAccessPoint     | ✅ (Multi region Access point Name)    | ✅ (Creation Time)                   | ❌    |    ❌   |
+| security-group              | SecurityGroup                | ✅ (Security group name)               | ✅ (Creation Time)                   | ✅    |    ❌   |
 | ses-configuration-set       | SesConfigurationset          | ✅ (Configuration set name)            | ❌                                   | ❌    |    ❌   |
 | ses-email-template          | SesEmailTemplates            | ✅ (Template Name)                     | ✅ (Creation Time)                   | ❌    |    ❌   |
 | ses-identity                | SesIdentity                  | ✅ (Identity -Mail/Domain)             | ❌                                   | ❌    |    ❌   |
diff --git a/aws/resource_registry.go b/aws/resource_registry.go
index 59ba4d6..231fd36 100644
--- a/aws/resource_registry.go
+++ b/aws/resource_registry.go
@@ -131,6 +131,8 @@ func getRegisteredRegionalResources() []AwsResource {
 		&resources.Route53CidrCollection{},
 		&resources.Route53TrafficPolicy{},
 		&resources.InternetGateway{},
+		&resources.NetworkInterface{},
+		&resources.SecurityGroup{},
 	}
 }
 
diff --git a/aws/resources/common.go b/aws/resources/common.go
new file mode 100644
index 0000000..979010e
--- /dev/null
+++ b/aws/resources/common.go
@@ -0,0 +1,10 @@
+package resources
+
+// The pattern we use for running the `cloud-nuke` tool is to split the AWS API calls
+// into batches when the function `NukeAllResources` is executed.
+// A batch max number has been chosen for most modules.
+// However, for ECS clusters there is no explicit limiting described in the AWS CLI docs.
+// Therefore this `maxBatchSize` here is set to 49 as a safe maximum.
+
+// This constant was moved into `common.go` since it was referenced across multiple resource types
+const maxBatchSize = 49
diff --git a/aws/resources/ec2_network_interface.go b/aws/resources/ec2_network_interface.go
new file mode 100644
index 0000000..230f9c0
--- /dev/null
+++ b/aws/resources/ec2_network_interface.go
@@ -0,0 +1,271 @@
+package resources
+
+import (
+	"context"
+	"time"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/service/ec2"
+	"github.com/gruntwork-io/cloud-nuke/config"
+	"github.com/gruntwork-io/cloud-nuke/logging"
+	"github.com/gruntwork-io/cloud-nuke/report"
+	"github.com/gruntwork-io/cloud-nuke/util"
+	"github.com/gruntwork-io/go-commons/errors"
+)
+
+func (ni *NetworkInterface) setFirstSeenTag(networkInterface ec2.NetworkInterface, value time.Time) error {
+	_, err := ni.Client.CreateTags(&ec2.CreateTagsInput{
+		Resources: []*string{networkInterface.NetworkInterfaceId},
+		Tags: []*ec2.Tag{
+			{
+				Key:   aws.String(util.FirstSeenTagKey),
+				Value: aws.String(util.FormatTimestamp(value)),
+			},
+		},
+	})
+	if err != nil {
+		return errors.WithStackTrace(err)
+	}
+
+	return nil
+}
+
+func (igw *NetworkInterface) getFirstSeenTag(networkInterface ec2.NetworkInterface) (*time.Time, error) {
+	tags := networkInterface.TagSet
+	for _, tag := range tags {
+		if util.IsFirstSeenTag(tag.Key) {
+			firstSeenTime, err := util.ParseTimestamp(tag.Value)
+			if err != nil {
+				return nil, errors.WithStackTrace(err)
+			}
+
+			return firstSeenTime, nil
+		}
+	}
+
+	return nil, nil
+}
+
+func shouldIncludeNetworkInterface(networkInterface *ec2.NetworkInterface, firstSeenTime *time.Time, configObj config.Config) bool {
+	var interfaceName string
+	// get the tags as map
+	tagMap := util.ConvertEC2TagsToMap(networkInterface.TagSet)
+	if name, ok := tagMap["Name"]; ok {
+		interfaceName = name
+	}
+	return configObj.NetworkInterface.ShouldInclude(config.ResourceValue{
+		Name: &interfaceName,
+		Tags: tagMap,
+		Time: firstSeenTime,
+	})
+}
+
+func (ni *NetworkInterface) getAll(_ context.Context, configObj config.Config) ([]*string, error) {
+	var identifiers []*string
+	resp, err := ni.Client.DescribeNetworkInterfaces(&ec2.DescribeNetworkInterfacesInput{})
+	if err != nil {
+		logging.Debugf("[Internet Gateway] Failed to list internet gateways: %s", err)
+		return nil, err
+	}
+
+	for _, networkInterface := range resp.NetworkInterfaces {
+		// check first seen tag
+		firstSeenTime, err := ni.getFirstSeenTag(*networkInterface)
+		if err != nil {
+			logging.Errorf(
+				"Unable to retrieve tags for Internet gateway: %s, with error: %s", *networkInterface.NetworkInterfaceId, err)
+			continue
+		}
+
+		// if the first seen tag is not there, then create one
+		if firstSeenTime == nil {
+			now := time.Now().UTC()
+			firstSeenTime = &now
+			if err := ni.setFirstSeenTag(*networkInterface, time.Now().UTC()); err != nil {
+				logging.Errorf(
+					"Unable to apply first seen tag Internet gateway: %s, with error: %s", *networkInterface.NetworkInterfaceId, err)
+				continue
+			}
+		}
+
+		if shouldIncludeNetworkInterface(networkInterface, firstSeenTime, configObj) {
+			identifiers = append(identifiers, networkInterface.NetworkInterfaceId)
+		}
+	}
+
+	// Check and verify the list of allowed nuke actions
+	ni.VerifyNukablePermissions(identifiers, func(id *string) error {
+		_, err := ni.Client.DeleteNetworkInterface(&ec2.DeleteNetworkInterfaceInput{
+			NetworkInterfaceId: id,
+			DryRun:             aws.Bool(true),
+		})
+		return err
+	})
+
+	return identifiers, nil
+}
+
+func (ni *NetworkInterface) nuke(id *string) error {
+	// detach network interfaces
+	if err := ni.detachNetworkInterface(id); err != nil {
+		return errors.WithStackTrace(err)
+	}
+	// nuking the network interface
+	if err := ni.nukeNetworkInterface(id); err != nil {
+		return errors.WithStackTrace(err)
+	}
+
+	return nil
+}
+
+func (ni *NetworkInterface) detachNetworkInterface(id *string) error {
+	logging.Debugf("Detaching network interface %s from instances ", aws.StringValue(id))
+
+	output, err := ni.Client.DescribeNetworkInterfaces(&ec2.DescribeNetworkInterfacesInput{
+		NetworkInterfaceIds: []*string{id},
+	})
+	if err != nil {
+		logging.Debugf("[Failed] Error describing network interface %s: %s", aws.StringValue(id), err)
+		return errors.WithStackTrace(err)
+	}
+
+	for _, networkInterface := range output.NetworkInterfaces {
+
+		// check there has some attachments
+		if networkInterface.Attachment == nil {
+			continue
+		}
+
+		// nuking the attached instance
+		// this will also remove the network interface
+		if err := ni.nukeInstance(networkInterface.Attachment.InstanceId); err != nil {
+			logging.Debugf("[Failed] Error nuking the attached instance %s on network interface %s %s", aws.StringValue(networkInterface.Attachment.InstanceId), aws.StringValue(id), err)
+			return errors.WithStackTrace(err)
+		}
+	}
+
+	logging.Debugf("[OK] successfully detached network interface associated on instances")
+
+	return nil
+}
+
+func (ni *NetworkInterface) releaseEIPs(instance *string) error {
+	logging.Debugf("Releasing Elastic IP address(s) associated on instance %s", aws.StringValue(instance))
+	// get the elastic ip's associated with the EC2's
+	output, err := ni.Client.DescribeAddresses(&ec2.DescribeAddressesInput{
+		Filters: []*ec2.Filter{
+			{
+				Name: aws.String("instance-id"),
+				Values: []*string{
+					instance,
+				},
+			},
+		},
+	})
+	if err != nil {
+		return err
+	}
+
+	for _, address := range output.Addresses {
+		if _, err := ni.Client.ReleaseAddress(&ec2.ReleaseAddressInput{
+			AllocationId: address.AllocationId,
+		}); err != nil {
+			logging.Debugf("An error happened while releasing the elastic ip address %s, error %v", aws.StringValue(address.AllocationId), err)
+			continue
+		}
+
+		logging.Debugf("Released Elastic IP address %s from instance %s", aws.StringValue(address.AllocationId), aws.StringValue(instance))
+	}
+
+	logging.Debugf("[OK] successfully released Elastic IP address(s) associated on instances")
+
+	return nil
+}
+
+func (ni *NetworkInterface) nukeInstance(id *string) error {
+
+	// Needs to release the elastic ips attached on the instance before nuking
+	if err := ni.releaseEIPs(id); err != nil {
+		logging.Debugf("[Failed EIP release] %s", err)
+		return errors.WithStackTrace(err)
+	}
+
+	// terminating the instance
+	if _, err := ni.Client.TerminateInstances(&ec2.TerminateInstancesInput{
+		InstanceIds: []*string{id},
+	}); err != nil {
+		logging.Debugf("[Failed] Ec2 termination %s", err)
+		return errors.WithStackTrace(err)
+	}
+
+	logging.Debugf("[Instance Termination] waiting to terminate instance %s", aws.StringValue(id))
+
+	// wait until the instance terminated.
+	if err := ni.Client.WaitUntilInstanceTerminated(&ec2.DescribeInstancesInput{
+		InstanceIds: []*string{id},
+	}); err != nil {
+		logging.Debugf("[Instance Termination Waiting] Failed to terminate instance %s : %s", *id, err)
+		return errors.WithStackTrace(err)
+	}
+	logging.Debugf("[OK] successfully nuked instance %v", aws.StringValue(id))
+
+	return nil
+}
+
+func (ni *NetworkInterface) nukeNetworkInterface(id *string) error {
+	logging.Debugf("Deleting network interface %s", aws.StringValue(id))
+
+	// If the network interface was attached to an instance, then when we remove the instance above, the network interface will also be removed.
+	// However, when we attempt to nuke the interface here, we may encounter an error such as InvalidNetworkInterfaceID.NotFound.
+	// In other situations, such as when the network interface hasn't been attached to any instances, we won't encounter this error.
+	//
+	// Note: We are handling the situation here by checking the error response from AWS.
+
+	_, err := ni.Client.DeleteNetworkInterface(&ec2.DeleteNetworkInterfaceInput{
+		NetworkInterfaceId: id,
+	})
+
+	// check the error exists and it is not the interfaceid not found
+	if err != nil && util.TransformAWSError(err) != util.ErrInterfaceIDNotFound {
+		logging.Debugf("[Failed] Error deleting network interface %s: %s", aws.StringValue(id), err)
+		return errors.WithStackTrace(err)
+	}
+	logging.Debugf("[Ok] network interface deleted successfully %s", aws.StringValue(id))
+
+	return nil
+}
+
+func (ni *NetworkInterface) nukeAll(identifiers []*string) error {
+	if len(identifiers) == 0 {
+		logging.Debugf("No network interface identifiers to nuke in region %s", ni.Region)
+		return nil
+	}
+
+	logging.Debugf("Deleting all network interface in region %s", ni.Region)
+	var deleted []*string
+
+	for _, id := range identifiers {
+		if nukable, err := ni.IsNukable(*id); !nukable {
+			logging.Debugf("[Skipping] %s nuke because %v", *id, err)
+			continue
+		}
+
+		err := ni.nuke(id)
+
+		// Record status of this resource
+		e := report.Entry{ // Use the 'r' alias to refer to the package
+			Identifier:   aws.StringValue(id),
+			ResourceType: "Network Interface",
+			Error:        err,
+		}
+		report.Record(e)
+
+		if err == nil {
+			deleted = append(deleted, id)
+		}
+	}
+
+	logging.Debugf("[OK] %d network interface(s) deleted in %s", len(deleted), ni.Region)
+
+	return nil
+}
diff --git a/aws/resources/ec2_network_interface_types.go b/aws/resources/ec2_network_interface_types.go
new file mode 100644
index 0000000..57add90
--- /dev/null
+++ b/aws/resources/ec2_network_interface_types.go
@@ -0,0 +1,54 @@
+package resources
+
+import (
+	"context"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/aws/session"
+	"github.com/aws/aws-sdk-go/service/ec2"
+	"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
+	"github.com/gruntwork-io/cloud-nuke/config"
+	"github.com/gruntwork-io/go-commons/errors"
+)
+
+type NetworkInterface struct {
+	BaseAwsResource
+	Client       ec2iface.EC2API
+	Region       string
+	InterfaceIds []string
+}
+
+func (ni *NetworkInterface) Init(session *session.Session) {
+	ni.BaseAwsResource.Init(session)
+	ni.Client = ec2.New(session)
+}
+
+func (ni *NetworkInterface) ResourceName() string {
+	return "network-interface"
+}
+
+func (ni *NetworkInterface) ResourceIdentifiers() []string {
+	return ni.InterfaceIds
+}
+
+func (ni *NetworkInterface) MaxBatchSize() int {
+	return 50
+}
+
+func (ni *NetworkInterface) GetAndSetIdentifiers(c context.Context, configObj config.Config) ([]string, error) {
+	identifiers, err := ni.getAll(c, configObj)
+	if err != nil {
+		return nil, err
+	}
+
+	ni.InterfaceIds = aws.StringValueSlice(identifiers)
+	return ni.InterfaceIds, nil
+}
+
+func (ni *NetworkInterface) Nuke(identifiers []string) error {
+	if err := ni.nukeAll(aws.StringSlice(identifiers)); err != nil {
+		return errors.WithStackTrace(err)
+	}
+
+	return nil
+}
diff --git a/aws/resources/ecs_cluster_types.go b/aws/resources/ecs_cluster_types.go
index 32a81b0..80e2355 100644
--- a/aws/resources/ecs_cluster_types.go
+++ b/aws/resources/ecs_cluster_types.go
@@ -11,13 +11,6 @@ import (
 	"github.com/gruntwork-io/go-commons/errors"
 )
 
-// The pattern we use for running the `cloud-nuke` tool is to split the AWS API calls
-// into batches when the function `NukeAllResources` is executed.
-// A batch max number has been chosen for most modules.
-// However, for ECS clusters there is no explicit limiting described in the AWS CLI docs.
-// Therefore this `maxBatchSize` here is set to 49 as a safe maximum.
-const maxBatchSize = 49
-
 // ECSClusters - Represents all ECS clusters found in a region
 type ECSClusters struct {
 	BaseAwsResource
diff --git a/aws/resources/security_group.go b/aws/resources/security_group.go
new file mode 100644
index 0000000..f640537
--- /dev/null
+++ b/aws/resources/security_group.go
@@ -0,0 +1,247 @@
+package resources
+
+import (
+	"context"
+	"time"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/service/ec2"
+	"github.com/gruntwork-io/cloud-nuke/config"
+	"github.com/gruntwork-io/cloud-nuke/logging"
+	r "github.com/gruntwork-io/cloud-nuke/report" // Alias the package as 'r'
+	"github.com/gruntwork-io/cloud-nuke/util"
+	"github.com/gruntwork-io/go-commons/errors"
+)
+
+func (sg *SecurityGroup) setFirstSeenTag(securityGroup ec2.SecurityGroup, value time.Time) error {
+	_, err := sg.Client.CreateTags(&ec2.CreateTagsInput{
+		Resources: []*string{securityGroup.GroupId},
+		Tags: []*ec2.Tag{
+			{
+				Key:   aws.String(util.FirstSeenTagKey),
+				Value: aws.String(util.FormatTimestamp(value)),
+			},
+		},
+	})
+	if err != nil {
+		return errors.WithStackTrace(err)
+	}
+	return nil
+}
+
+func (sg *SecurityGroup) getFirstSeenTag(securityGroup ec2.SecurityGroup) (*time.Time, error) {
+	tags := securityGroup.Tags
+	for _, tag := range tags {
+		if util.IsFirstSeenTag(tag.Key) {
+			firstSeenTime, err := util.ParseTimestamp(tag.Value)
+			if err != nil {
+				return nil, errors.WithStackTrace(err)
+			}
+
+			return firstSeenTime, nil
+		}
+	}
+	return nil, nil
+}
+
+// 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
+	return configObj.SecurityGroup.ShouldInclude(config.ResourceValue{
+		Name: groupName,
+		Tags: util.ConvertEC2TagsToMap(sg.Tags),
+		Time: firstSeenTime,
+	})
+}
+
+// getAll retrieves all security group identifiers based on the provided configuration.
+func (sg *SecurityGroup) getAll(_ context.Context, configObj config.Config) ([]*string, error) {
+	var identifiers []*string
+
+	resp, err := sg.Client.DescribeSecurityGroups(&ec2.DescribeSecurityGroupsInput{})
+	if err != nil {
+		logging.Debugf("[Security Group] Failed to list security groups: %s", err)
+		return nil, errors.WithStackTrace(err)
+	}
+
+	for _, group := range resp.SecurityGroups {
+		// check first seen tag
+		firstSeenTime, err := sg.getFirstSeenTag(*group)
+		if err != nil {
+			logging.Errorf(
+				"Unable to retrieve tags for Security group: %s, with error: %s", *group.GroupId, err)
+			continue
+		}
+
+		// if the first seen tag is not there, then create one
+		if firstSeenTime == nil {
+			now := time.Now().UTC()
+			firstSeenTime = &now
+			if err := sg.setFirstSeenTag(*group, time.Now().UTC()); err != nil {
+				logging.Errorf(
+					"Unable to apply first seen tag Security group: %s, with error: %s", *group.GroupId, err)
+				continue
+			}
+		}
+		if shouldIncludeSecurityGroup(group, firstSeenTime, configObj) {
+			identifiers = append(identifiers, group.GroupId)
+		}
+	}
+
+	// Check and verify the list of allowed nuke actions
+	sg.VerifyNukablePermissions(identifiers, func(id *string) error {
+		_, err := sg.Client.DeleteSecurityGroup(&ec2.DeleteSecurityGroupInput{
+			GroupId: id,
+			DryRun:  aws.Bool(true),
+		})
+		return err
+	})
+
+	return identifiers, nil
+}
+
+func (sg *SecurityGroup) nuke(id *string) error {
+
+	if err := sg.terminateInstancesAssociatedWithSecurityGroup(*id); err != nil {
+		return errors.WithStackTrace(err)
+	}
+
+	if err := sg.nukeSecurityGroup(*id); err != nil {
+		return errors.WithStackTrace(err)
+	}
+	return nil
+}
+
+func (sg *SecurityGroup) terminateInstancesAssociatedWithSecurityGroup(id string) error {
+
+	resp, err := sg.Client.DescribeInstances(&ec2.DescribeInstancesInput{
+		Filters: []*ec2.Filter{
+			{
+				Name:   aws.String("instance.group-id"),
+				Values: []*string{aws.String(id)},
+			},
+		},
+	})
+	if err != nil {
+		logging.Debugf("[Security Group] Failed to describe instances associated with security group %s: %s", id, err)
+		return errors.WithStackTrace(err)
+	}
+
+	for _, reservation := range resp.Reservations {
+		for _, instance := range reservation.Instances {
+			instanceID := aws.StringValue(instance.InstanceId)
+
+			// Needs to release the elastic ips attached on the instance before nuking
+			if err := sg.releaseEIPs([]*string{instance.InstanceId}); err != nil {
+				logging.Debugf("[Failed EIP release] %s", err)
+				return errors.WithStackTrace(err)
+			}
+
+			// terminating the instances which used this security group
+			if _, err := sg.Client.TerminateInstances(&ec2.TerminateInstancesInput{
+				InstanceIds: []*string{aws.String(instanceID)},
+			}); err != nil {
+				logging.Debugf("[Failed] Ec2 termination %s", err)
+				return errors.WithStackTrace(err)
+			}
+
+			logging.Debugf("[Instance Termination] waiting to terminate instance %s", instanceID)
+
+			// wait until the instance terminated.
+			if err := sg.Client.WaitUntilInstanceTerminated(&ec2.DescribeInstancesInput{
+				InstanceIds: []*string{aws.String(instanceID)},
+			}); err != nil {
+				logging.Debugf("[Security Group] Failed to terminate instance %s associated with security group %s: %s", instanceID, id, err)
+				return errors.WithStackTrace(err)
+			}
+
+			logging.Debugf("Terminated instance %s associated with security group %s", instanceID, id)
+		}
+	}
+
+	return nil
+}
+
+func (sg *SecurityGroup) releaseEIPs(instanceIds []*string) error {
+	logging.Debugf("Releasing Elastic IP address(s) associated on instances")
+	for _, instanceID := range instanceIds {
+
+		// get the elastic ip's associated with the EC2's
+		output, err := sg.Client.DescribeAddresses(&ec2.DescribeAddressesInput{
+			Filters: []*ec2.Filter{
+				{
+					Name: aws.String("instance-id"),
+					Values: []*string{
+						instanceID,
+					},
+				},
+			},
+		})
+		if err != nil {
+			return err
+		}
+
+		for _, address := range output.Addresses {
+			if _, err := sg.Client.ReleaseAddress(&ec2.ReleaseAddressInput{
+				AllocationId: address.AllocationId,
+			}); err != nil {
+				logging.Debugf("An error happened while releasing the elastic ip address %s, error %v", *address.AllocationId, err)
+				continue
+			}
+
+			logging.Debugf("Released Elastic IP address %s from instance %s", *address.AllocationId, *instanceID)
+		}
+	}
+
+	return nil
+}
+
+func (sg *SecurityGroup) nukeSecurityGroup(id string) error {
+	if _, err := sg.Client.DeleteSecurityGroup(&ec2.DeleteSecurityGroupInput{
+		GroupId: aws.String(id),
+	}); err != nil {
+		logging.Debugf("[Security Group] Failed to delete security group %s: %s", id, err)
+		return errors.WithStackTrace(err)
+	}
+	logging.Debugf("Deleted security group %s", id)
+
+	return nil
+}
+
+func (sg *SecurityGroup) nukeAll(identifiers []*string) error {
+	if len(identifiers) == 0 {
+		logging.Debugf("No security group identifiers to nuke in region %s", sg.Region)
+		return nil
+	}
+
+	logging.Debugf("Deleting all security groups in region %s", sg.Region)
+	var deletedGroups []*string
+
+	for _, id := range identifiers {
+
+		if nukable, err := sg.IsNukable(*id); !nukable {
+			logging.Debugf("[Skipping] %s nuke because %v", *id, err)
+			continue
+		}
+
+		err := sg.nuke(id)
+		// Record status of this resource
+		e := r.Entry{
+			Identifier:   aws.StringValue(id),
+			ResourceType: "Security Group",
+			Error:        err,
+		}
+		r.Record(e)
+
+		if err != nil {
+			logging.Debugf("[Failed] Error deleting security group %s: %s", *id, err)
+		} else {
+			deletedGroups = append(deletedGroups, id)
+			logging.Debugf("Deleted security group: %s", *id)
+		}
+	}
+
+	logging.Debugf("[OK] %d security group(s) deleted in %s", len(deletedGroups), sg.Region)
+
+	return nil
+}
diff --git a/aws/resources/security_group_types.go b/aws/resources/security_group_types.go
new file mode 100644
index 0000000..00b4995
--- /dev/null
+++ b/aws/resources/security_group_types.go
@@ -0,0 +1,54 @@
+package resources
+
+import (
+	"context"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/aws/session"
+	"github.com/aws/aws-sdk-go/service/ec2"
+	"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
+	"github.com/gruntwork-io/cloud-nuke/config"
+	"github.com/gruntwork-io/go-commons/errors"
+)
+
+type SecurityGroup struct {
+	BaseAwsResource
+	Client         ec2iface.EC2API
+	Region         string
+	SecurityGroups []string
+}
+
+func (sg *SecurityGroup) Init(session *session.Session) {
+	sg.BaseAwsResource.Init(session)
+	sg.Client = ec2.New(session)
+}
+
+func (sg *SecurityGroup) ResourceName() string {
+	return "security-group"
+}
+
+func (sg *SecurityGroup) ResourceIdentifiers() []string {
+	return sg.SecurityGroups
+}
+
+func (sg *SecurityGroup) MaxBatchSize() int {
+	return 50
+}
+
+func (sg *SecurityGroup) GetAndSetIdentifiers(c context.Context, configObj config.Config) ([]string, error) {
+	identifiers, err := sg.getAll(c, configObj)
+	if err != nil {
+		return nil, err
+	}
+
+	sg.SecurityGroups = aws.StringValueSlice(identifiers)
+	return sg.SecurityGroups, nil
+}
+
+func (sg *SecurityGroup) Nuke(identifiers []string) error {
+	if err := sg.nukeAll(aws.StringSlice(identifiers)); err != nil {
+		return errors.WithStackTrace(err)
+	}
+
+	return nil
+}
diff --git a/config/config.go b/config/config.go
index dcf6a20..efcfa06 100644
--- a/config/config.go
+++ b/config/config.go
@@ -102,6 +102,8 @@ type Config struct {
 	Route53CIDRCollection           ResourceType               `yaml:"Route53CIDRCollection"`
 	Route53TrafficPolicy            ResourceType               `yaml:"Route53TrafficPolicy"`
 	InternetGateway                 ResourceType               `yaml:"InternetGateway"`
+	SecurityGroup                   ResourceType               `yaml:"SecurityGroup"`
+	NetworkInterface                ResourceType               `yaml:"NetworkInterface"`
 }
 
 func (c *Config) addTimeAfterFilter(timeFilter *time.Time, fieldName string) {
diff --git a/util/error.go b/util/error.go
index 8a41df9..76e78b0 100644
--- a/util/error.go
+++ b/util/error.go
@@ -11,6 +11,7 @@ import (
 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")
 
 const AWsUnauthorizedError string = "UnauthorizedOperation"
 const AwsDryRunSuccess string = "Request would have succeeded, but DryRun flag is set."
@@ -28,6 +29,10 @@ func TransformAWSError(err error) error {
 		return ErrContextExecutionTimeout
 	}
 
+	if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "InvalidNetworkInterfaceID.NotFound" {
+		return ErrInterfaceIDNotFound
+	}
+
 	if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "DryRunOperation" && awsErr.Message() == AwsDryRunSuccess {
 		return nil
 	}


Test patch:
diff --git a/aws/resources/ec2_network_interface_test.go b/aws/resources/ec2_network_interface_test.go
new file mode 100644
index 0000000..d85823d
--- /dev/null
+++ b/aws/resources/ec2_network_interface_test.go
@@ -0,0 +1,222 @@
+package resources
+
+import (
+	"context"
+	"regexp"
+	"testing"
+	"time"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/service/ec2"
+	"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
+	"github.com/gruntwork-io/cloud-nuke/config"
+	"github.com/gruntwork-io/cloud-nuke/telemetry"
+	"github.com/gruntwork-io/cloud-nuke/util"
+	"github.com/stretchr/testify/require"
+)
+
+type mockedNetworkInterface struct {
+	BaseAwsResource
+	ec2iface.EC2API
+	DescribeNetworkInterfacesOutput ec2.DescribeNetworkInterfacesOutput
+	DeleteNetworkInterfaceOutput    ec2.DeleteNetworkInterfaceOutput
+	DescribeAddressesOutput         ec2.DescribeAddressesOutput
+	TerminateInstancesOutput        ec2.TerminateInstancesOutput
+	ReleaseAddressOutput            ec2.ReleaseAddressOutput
+}
+
+func (m mockedNetworkInterface) DescribeNetworkInterfaces(*ec2.DescribeNetworkInterfacesInput) (*ec2.DescribeNetworkInterfacesOutput, error) {
+	return &m.DescribeNetworkInterfacesOutput, nil
+}
+
+func (m mockedNetworkInterface) DeleteNetworkInterface(*ec2.DeleteNetworkInterfaceInput) (*ec2.DeleteNetworkInterfaceOutput, error) {
+	return &m.DeleteNetworkInterfaceOutput, nil
+}
+
+func (m mockedNetworkInterface) DescribeAddresses(*ec2.DescribeAddressesInput) (*ec2.DescribeAddressesOutput, error) {
+	return &m.DescribeAddressesOutput, nil
+}
+
+func (m mockedNetworkInterface) ReleaseAddress(*ec2.ReleaseAddressInput) (*ec2.ReleaseAddressOutput, error) {
+	return &m.ReleaseAddressOutput, nil
+}
+
+func (m mockedNetworkInterface) TerminateInstances(*ec2.TerminateInstancesInput) (*ec2.TerminateInstancesOutput, error) {
+	return &m.TerminateInstancesOutput, nil
+}
+
+func (m mockedNetworkInterface) WaitUntilInstanceTerminated(*ec2.DescribeInstancesInput) error {
+	return nil
+}
+
+func TestNetworkInterface_GetAll(t *testing.T) {
+	telemetry.InitTelemetry("cloud-nuke", "")
+
+	var (
+		now     = time.Now()
+		testId1 = "eni-09e36c45cbdbfb001"
+		testId2 = "eni-09e36c45cbdbfb002"
+
+		testName1 = "cloud-nuke-eni-001"
+		testName2 = "cloud-nuke-eni-002"
+	)
+
+	resourceObject := NetworkInterface{
+		Client: mockedNetworkInterface{
+			DescribeNetworkInterfacesOutput: ec2.DescribeNetworkInterfacesOutput{
+				NetworkInterfaces: []*ec2.NetworkInterface{
+					{
+						NetworkInterfaceId: aws.String(testId1),
+						TagSet: []*ec2.Tag{
+							{
+								Key:   aws.String("Name"),
+								Value: aws.String(testName1),
+							},
+							{
+								Key:   aws.String(util.FirstSeenTagKey),
+								Value: aws.String(util.FormatTimestamp(now)),
+							},
+						},
+					},
+					{
+						NetworkInterfaceId: aws.String(testId2),
+						TagSet: []*ec2.Tag{
+							{
+								Key:   aws.String("Name"),
+								Value: aws.String(testName2),
+							},
+							{
+								Key:   aws.String(util.FirstSeenTagKey),
+								Value: aws.String(util.FormatTimestamp(now.Add(1 * time.Hour))),
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+	resourceObject.BaseAwsResource.Init(nil)
+
+	tests := map[string]struct {
+		configObj config.ResourceType
+		expected  []string
+	}{
+		"emptyFilter": {
+			configObj: config.ResourceType{},
+			expected:  []string{testId1, testId2},
+		},
+		"nameExclusionFilter": {
+			configObj: config.ResourceType{
+				ExcludeRule: config.FilterRule{
+					NamesRegExp: []config.Expression{{
+						RE: *regexp.MustCompile(testName1),
+					}}},
+			},
+			expected: []string{testId2},
+		},
+		"nameInclusionFilter": {
+			configObj: config.ResourceType{
+				IncludeRule: config.FilterRule{
+					NamesRegExp: []config.Expression{{
+						RE: *regexp.MustCompile(testName1),
+					}}},
+			},
+			expected: []string{testId1},
+		},
+		"timeAfterExclusionFilter": {
+			configObj: config.ResourceType{
+				ExcludeRule: config.FilterRule{
+					TimeAfter: aws.Time(now),
+				}},
+			expected: []string{
+				testId1,
+			},
+		},
+	}
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			t.Parallel()
+			names, err := resourceObject.getAll(context.Background(), config.Config{
+				NetworkInterface: tc.configObj,
+			})
+			require.NoError(t, err)
+			require.Equal(t, tc.expected, aws.StringValueSlice(names))
+		})
+	}
+
+}
+
+func TestNetworkInterface_NukeAll(t *testing.T) {
+	telemetry.InitTelemetry("cloud-nuke", "")
+	t.Parallel()
+
+	var (
+		testId1 = "eni-09e36c45cbdbfb001"
+		testId2 = "eni-09e36c45cbdbfb002"
+
+		testName1 = "cloud-nuke-eni-001"
+		testName2 = "cloud-nuke-eni-002"
+	)
+
+	resourceObject := NetworkInterface{
+		BaseAwsResource: BaseAwsResource{
+			Nukables: map[string]error{
+				testId1: nil,
+				testId2: nil,
+			},
+		},
+		Client: mockedNetworkInterface{
+			DeleteNetworkInterfaceOutput: ec2.DeleteNetworkInterfaceOutput{},
+			DescribeNetworkInterfacesOutput: ec2.DescribeNetworkInterfacesOutput{
+				NetworkInterfaces: []*ec2.NetworkInterface{
+					{
+						NetworkInterfaceId: aws.String(testId1),
+						TagSet: []*ec2.Tag{
+							{
+								Key:   aws.String("Name"),
+								Value: aws.String(testName1),
+							},
+						},
+						Attachment: &ec2.NetworkInterfaceAttachment{
+							AttachmentId: aws.String("network-attachment-09e36c45cbdbfb001"),
+							InstanceId:   aws.String("ec2-instance-09e36c45cbdbfb001"),
+						},
+					},
+					{
+						NetworkInterfaceId: aws.String(testId2),
+						TagSet: []*ec2.Tag{
+							{
+								Key:   aws.String("Name"),
+								Value: aws.String(testName2),
+							},
+						},
+						Attachment: &ec2.NetworkInterfaceAttachment{
+							AttachmentId: aws.String("network-attachment-09e36c45cbdbfb002"),
+							InstanceId:   aws.String("ec2-instance-09e36c45cbdbfb002"),
+						},
+					},
+				},
+			},
+			DescribeAddressesOutput: ec2.DescribeAddressesOutput{
+				Addresses: []*ec2.Address{
+					{
+						AllocationId: aws.String("ec2-addr-alloc-09e36c45cbdbfb001"),
+						InstanceId:   aws.String("ec2-instance-09e36c45cbdbfb001"),
+					},
+					{
+						AllocationId: aws.String("ec2-addr-alloc-09e36c45cbdbfb002"),
+						InstanceId:   aws.String("ec2-instance-09e36c45cbdbfb002"),
+					},
+				},
+			},
+			TerminateInstancesOutput: ec2.TerminateInstancesOutput{},
+			ReleaseAddressOutput:     ec2.ReleaseAddressOutput{},
+		},
+	}
+
+	err := resourceObject.nukeAll([]*string{
+		aws.String(testId1),
+		aws.String(testId2),
+	})
+	require.NoError(t, err)
+}
diff --git a/aws/resources/security_group_test.go b/aws/resources/security_group_test.go
new file mode 100644
index 0000000..85e8136
--- /dev/null
+++ b/aws/resources/security_group_test.go
@@ -0,0 +1,142 @@
+package resources
+
+import (
+	"context"
+	"regexp"
+	"testing"
+	"time"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/service/ec2"
+	"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
+	"github.com/gruntwork-io/cloud-nuke/config"
+	"github.com/gruntwork-io/cloud-nuke/telemetry"
+	"github.com/gruntwork-io/cloud-nuke/util"
+	"github.com/stretchr/testify/require"
+)
+
+type mockedSecurityGroup struct {
+	BaseAwsResource
+	ec2iface.EC2API
+	DescribeSecurityGroupsOutput ec2.DescribeSecurityGroupsOutput
+	DeleteSecurityGroupOutput    ec2.DeleteSecurityGroupOutput
+	DescribeInstancesOutput      ec2.DescribeInstancesOutput
+}
+
+func (m mockedSecurityGroup) DescribeSecurityGroups(input *ec2.DescribeSecurityGroupsInput) (*ec2.DescribeSecurityGroupsOutput, error) {
+	return &m.DescribeSecurityGroupsOutput, nil
+}
+
+func (m mockedSecurityGroup) DescribeInstances(input *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error) {
+	return &m.DescribeInstancesOutput, nil
+}
+
+func (m mockedSecurityGroup) DeleteSecurityGroup(input *ec2.DeleteSecurityGroupInput) (*ec2.DeleteSecurityGroupOutput, error) {
+	return &m.DeleteSecurityGroupOutput, nil
+}
+
+func TestSecurityGroup_GetAll(t *testing.T) {
+
+	var (
+		testId1   = "sg-08f2b91f81265ab7b001"
+		testId2   = "sg-08f2b91f81265ab7b002"
+		testName1 = "cloud-nuke-sg-001"
+		testName2 = "cloud-nuke-sg-002"
+		now       = time.Now()
+	)
+
+	sg := SecurityGroup{
+		BaseAwsResource: BaseAwsResource{
+			Nukables: map[string]error{
+				testId1: nil,
+				testId2: nil,
+			},
+		},
+		Client: &mockedSecurityGroup{
+			DescribeSecurityGroupsOutput: ec2.DescribeSecurityGroupsOutput{
+				SecurityGroups: []*ec2.SecurityGroup{
+					{
+						GroupId:   aws.String(testId1),
+						GroupName: aws.String(testName1),
+						Tags: []*ec2.Tag{
+							{
+								Key:   aws.String("Name"),
+								Value: aws.String(testName1),
+							},
+							{
+								Key:   aws.String(util.FirstSeenTagKey),
+								Value: aws.String(util.FormatTimestamp(now)),
+							},
+						},
+					},
+					{
+						GroupId:   aws.String(testId2),
+						GroupName: aws.String(testName2),
+						Tags: []*ec2.Tag{
+							{
+								Key:   aws.String("Name"),
+								Value: aws.String(testName2),
+							},
+							{
+								Key:   aws.String(util.FirstSeenTagKey),
+								Value: aws.String(util.FormatTimestamp(now.Add(1 * time.Hour))),
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	tests := map[string]struct {
+		configObj config.ResourceType
+		expected  []string
+	}{
+		"emptyFilter": {
+			configObj: config.ResourceType{},
+			expected:  []string{testId1, testId2},
+		},
+		"nameExclusionFilter": {
+			configObj: 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),
+				}},
+			expected: []string{testId1},
+		},
+	}
+
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			t.Parallel()
+			names, err := sg.getAll(context.Background(), config.Config{
+				SecurityGroup: tc.configObj,
+			})
+			require.NoError(t, err)
+			require.Equal(t, tc.expected, aws.StringValueSlice(names))
+		})
+	}
+}
+
+func Test_NukeAll(t *testing.T) {
+	telemetry.InitTelemetry("cloud-nuke", "")
+	t.Parallel()
+
+	er := SecurityGroup{
+		Client: &mockedSecurityGroup{
+			DeleteSecurityGroupOutput: ec2.DeleteSecurityGroupOutput{},
+		},
+	}
+
+	identifiers := []*string{aws.String("sg-1")}
+	err := er.nukeAll(identifiers)
+	require.NoError(t, err)
+}
diff --git a/config/config_test.go b/config/config_test.go
index 58bde00..c49abb4 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -100,6 +100,8 @@ func emptyConfig() *Config {
 		ResourceType{FilterRule{}, FilterRule{}, ""},
 		ResourceType{FilterRule{}, FilterRule{}, ""},
 		ResourceType{FilterRule{}, FilterRule{}, ""},
+		ResourceType{FilterRule{}, FilterRule{}, ""},
+		ResourceType{FilterRule{}, FilterRule{}, ""},
 	}
 }