Repository: gruntwork-io/cloud-nuke
Pull request #690 (2024-04-25 02:24:42)

Nuke Network Resource Policy
<!-- Prepend '[WIP]' to the title if this PR is still a work-in-progress. Remove it when it is ready for review! -->

## Description

Fixes https://github.com/gruntwork-io/cloud-nuke/issues/684

<!-- 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/README.md b/README.md
index 55e044e..c260b0b 100644
--- a/README.md
+++ b/README.md
@@ -103,6 +103,7 @@ Cloud-nuke suppports 🔎 inspecting and 🔥💀 deleting the following AWS res
 | NetworkFirewall         | Network Firewall Policy                                  |
 | NetworkFirewall         | Network Firewall Rule Group                              |
 | NetworkFirewall         | Network Firewall TLS inspection configuration            |
+| NetworkFirewall         | Network Firewall Resource Policy 						             |
 
 > **WARNING:** The RDS APIs also interact with neptune and document db resources.
 > Running `cloud-nuke aws --resource-type rds` without a config file will remove any neptune and document db resources
@@ -625,6 +626,7 @@ of the file that are supported are listed here.
 | network-firewall-policy		  | NetworkFirewallPolicy        | ✅ (Firewall Policy name)       			  | ✅ (First Seen Tag Time)             | ✅    |    ❌   |
 | network-firewall-rule-group | NetworkFirewallRuleGroup     | ✅ (Firewall Rule group name)   			  | ✅ (First Seen Tag Time)             | ✅    |    ❌   |
 | network-firewall-tls-config | NetworkFirewallTLSConfig     | ✅ (Firewall TLS config name)   			  | ✅ (First Seen Tag Time)             | ✅    |    ❌   |
+| network-firewall-resource-policy | NetworkFirewallResourcePolicy     | ✅ (Firewall Resource Policy ARN)   			  |  ❌             |  ❌    |    ❌   |
 
 
 ### How to Use
diff --git a/aws/resource_registry.go b/aws/resource_registry.go
index 1410d51..1f646b4 100644
--- a/aws/resource_registry.go
+++ b/aws/resource_registry.go
@@ -138,6 +138,7 @@ func getRegisteredRegionalResources() []AwsResource {
 		&resources.NetworkFirewallPolicy{},
 		&resources.NetworkFirewallRuleGroup{},
 		&resources.NetworkFirewallTLSConfig{},
+		&resources.NetworkFirewallResourcePolicy{},
 	}
 }
 
diff --git a/aws/resources/network_firewall_resource_policy.go b/aws/resources/network_firewall_resource_policy.go
new file mode 100644
index 0000000..57f28af
--- /dev/null
+++ b/aws/resources/network_firewall_resource_policy.go
@@ -0,0 +1,97 @@
+package resources
+
+import (
+	"context"
+
+	awsgo "github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/service/networkfirewall"
+	"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"
+)
+
+// The Network Firewall service supports only one type of resource-based policy called a resource policy, which is attached to a shared firewall policy or rule group
+// References :
+// - https://docs.aws.amazon.com/network-firewall/latest/developerguide/security_iam_resource-based-policy-examples.html
+// - https://docs.aws.amazon.com/network-firewall/latest/developerguide/sharing.html
+// - https://docs.aws.amazon.com/ram/latest/userguide/what-is.html
+func (nfw *NetworkFirewallResourcePolicy) getAll(_ context.Context, configObj config.Config) ([]*string, error) {
+	var identifiers []*string
+
+	var resourceArns []*string
+	// list the firewall policies and rule group
+
+	{
+		policyMeta, err := nfw.Client.ListFirewallPolicies(nil)
+		if err != nil {
+			return nil, errors.WithStackTrace(err)
+		}
+
+		for _, policy := range policyMeta.FirewallPolicies {
+			resourceArns = append(resourceArns, policy.Arn)
+		}
+		groupMeta, err := nfw.Client.ListRuleGroups(nil)
+		if err != nil {
+			return nil, errors.WithStackTrace(err)
+		}
+		for _, group := range groupMeta.RuleGroups {
+			resourceArns = append(resourceArns, group.Arn)
+		}
+	}
+
+	// get the resource policies attached on these arns
+	for _, arn := range resourceArns {
+		output, err := nfw.Client.DescribeResourcePolicy(&networkfirewall.DescribeResourcePolicyInput{
+			ResourceArn: arn,
+		})
+		if err != nil && util.TransformAWSError(err) != util.ErrResourceNotFoundException {
+			return nil, errors.WithStackTrace(err)
+		}
+
+		// if the policy exists for a resource
+		if output.Policy != nil {
+			if configObj.NetworkFirewallResourcePolicy.ShouldInclude(config.ResourceValue{
+				Name: arn,
+			}) {
+				identifiers = append(identifiers, arn)
+			}
+		}
+	}
+	return identifiers, nil
+}
+
+func (nfw *NetworkFirewallResourcePolicy) nukeAll(identifiers []*string) error {
+	if len(identifiers) == 0 {
+		logging.Debugf("No Network Firewall resource policy to nuke in region %s", nfw.Region)
+		return nil
+	}
+
+	logging.Debugf("Deleting Network firewall resource policy in region %s", nfw.Region)
+	var deleted []*string
+
+	for _, id := range identifiers {
+		_, err := nfw.Client.DeleteResourcePolicy(&networkfirewall.DeleteResourcePolicyInput{
+			ResourceArn: id,
+		})
+
+		// Record status of this resource
+		e := report.Entry{
+			Identifier:   awsgo.StringValue(id),
+			ResourceType: "Network Firewall Resource policy",
+			Error:        err,
+		}
+		report.Record(e)
+
+		if err != nil {
+			logging.Debugf("[Failed] %s", err)
+		} else {
+			deleted = append(deleted, id)
+		}
+	}
+
+	logging.Debugf("[OK] %d Network Resource Policy(s) deleted in %s", len(deleted), nfw.Region)
+
+	return nil
+}
diff --git a/aws/resources/network_firewall_resource_policy_types.go b/aws/resources/network_firewall_resource_policy_types.go
new file mode 100644
index 0000000..557beac
--- /dev/null
+++ b/aws/resources/network_firewall_resource_policy_types.go
@@ -0,0 +1,58 @@
+package resources
+
+import (
+	"context"
+
+	awsgo "github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/aws/session"
+	"github.com/aws/aws-sdk-go/service/networkfirewall"
+	"github.com/aws/aws-sdk-go/service/networkfirewall/networkfirewalliface"
+	"github.com/gruntwork-io/cloud-nuke/config"
+	"github.com/gruntwork-io/go-commons/errors"
+)
+
+type NetworkFirewallResourcePolicy struct {
+	BaseAwsResource
+	Client      networkfirewalliface.NetworkFirewallAPI
+	Region      string
+	Identifiers []string
+}
+
+func (nfrp *NetworkFirewallResourcePolicy) Init(session *session.Session) {
+	nfrp.Client = networkfirewall.New(session)
+}
+
+// ResourceName - the simple name of the aws resource
+func (nfrp *NetworkFirewallResourcePolicy) ResourceName() string {
+	return "network-firewall-resource-policy"
+}
+
+func (nfrp *NetworkFirewallResourcePolicy) ResourceIdentifiers() []string {
+	return nfrp.Identifiers
+}
+
+func (nfrp *NetworkFirewallResourcePolicy) MaxBatchSize() int {
+	// Tentative batch size to ensure AWS doesn't throttle. Note that nat gateway does not support bulk delete, so
+	// we will be deleting this many in parallel using go routines. We conservatively pick 10 here, both to limit
+	// overloading the runtime and to avoid AWS throttling with many API calls.
+	return 10
+}
+
+func (nfrp *NetworkFirewallResourcePolicy) GetAndSetIdentifiers(c context.Context, configObj config.Config) ([]string, error) {
+	identifiers, err := nfrp.getAll(c, configObj)
+	if err != nil {
+		return nil, err
+	}
+
+	nfrp.Identifiers = awsgo.StringValueSlice(identifiers)
+	return nfrp.Identifiers, nil
+}
+
+// Nuke - nuke 'em all!!!
+func (nfrp *NetworkFirewallResourcePolicy) Nuke(identifiers []string) error {
+	if err := nfrp.nukeAll(awsgo.StringSlice(identifiers)); err != nil {
+		return errors.WithStackTrace(err)
+	}
+
+	return nil
+}
diff --git a/config/config.go b/config/config.go
index 6c4f44d..b20a05a 100644
--- a/config/config.go
+++ b/config/config.go
@@ -109,6 +109,7 @@ type Config struct {
 	NetworkFirewallPolicy           ResourceType               `yaml:"NetworkFirewallPolicy"`
 	NetworkFirewallRuleGroup        ResourceType               `yaml:"NetworkFirewallRuleGroup"`
 	NetworkFirewallTLSConfig        ResourceType               `yaml:"NetworkFirewallTLSConfig"`
+	NetworkFirewallResourcePolicy   ResourceType               `yaml:"NetworkFirewallResourcePolicy"`
 }
 
 func (c *Config) addTimeAfterFilter(timeFilter *time.Time, fieldName string) {
diff --git a/util/error.go b/util/error.go
index 735550e..ec13d5d 100644
--- a/util/error.go
+++ b/util/error.go
@@ -14,6 +14,7 @@ var ErrContextExecutionTimeout = errors.New("error:EXECUTION_TIMEOUT")
 var ErrInterfaceIDNotFound = errors.New("error:InterfaceIdNotFound")
 var ErrInvalidPermisionNotFound = errors.New("error:InvalidPermission.NotFound")
 var ErrDeleteProtectionEnabled = errors.New("error:DeleteProtectionEnabled")
+var ErrResourceNotFoundException = errors.New("error:ErrResourceNotFoundException")
 
 const AWsUnauthorizedError string = "UnauthorizedOperation"
 const AwsDryRunSuccess string = "Request would have succeeded, but DryRun flag is set."
@@ -41,6 +42,9 @@ func TransformAWSError(err error) error {
 	if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "InvalidPermission.NotFound" {
 		return ErrInvalidPermisionNotFound
 	}
+	if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "ResourceNotFoundException" {
+		return ErrResourceNotFoundException
+	}
 
 	return err
 


Test patch:
diff --git a/aws/resources/network_firewall_resource_policy_test.go b/aws/resources/network_firewall_resource_policy_test.go
new file mode 100644
index 0000000..6d20658
--- /dev/null
+++ b/aws/resources/network_firewall_resource_policy_test.go
@@ -0,0 +1,122 @@
+package resources
+
+import (
+	"context"
+	"regexp"
+	"testing"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/service/networkfirewall"
+	"github.com/aws/aws-sdk-go/service/networkfirewall/networkfirewalliface"
+
+	awsgo "github.com/aws/aws-sdk-go/aws"
+	"github.com/gruntwork-io/cloud-nuke/config"
+	"github.com/stretchr/testify/require"
+)
+
+type mockedNetworkFirewallResourcePolicy struct {
+	networkfirewalliface.NetworkFirewallAPI
+	ListFirewallPoliciesOutput   networkfirewall.ListFirewallPoliciesOutput
+	ListRuleGroupsOutput         networkfirewall.ListRuleGroupsOutput
+	DeleteResourcePolicyOutput   networkfirewall.DeleteResourcePolicyOutput
+	DescribeResourcePolicyOutput networkfirewall.DescribeResourcePolicyOutput
+}
+
+func (m mockedNetworkFirewallResourcePolicy) ListFirewallPolicies(*networkfirewall.ListFirewallPoliciesInput) (*networkfirewall.ListFirewallPoliciesOutput, error) {
+	return &m.ListFirewallPoliciesOutput, nil
+}
+
+func (m mockedNetworkFirewallResourcePolicy) ListRuleGroups(*networkfirewall.ListRuleGroupsInput) (*networkfirewall.ListRuleGroupsOutput, error) {
+	return &m.ListRuleGroupsOutput, nil
+}
+func (m mockedNetworkFirewallResourcePolicy) DeleteResourcePolicy(*networkfirewall.DeleteResourcePolicyInput) (*networkfirewall.DeleteResourcePolicyOutput, error) {
+	return &m.DeleteResourcePolicyOutput, nil
+}
+
+func (m mockedNetworkFirewallResourcePolicy) DescribeResourcePolicy(*networkfirewall.DescribeResourcePolicyInput) (*networkfirewall.DescribeResourcePolicyOutput, error) {
+	return &m.DescribeResourcePolicyOutput, nil
+}
+
+func TestNetworkFirewallResourcePolicy_GetAll(t *testing.T) {
+
+	t.Parallel()
+
+	var (
+		policy1 = "test-network-firewall-policy-1"
+		policy2 = "test-network-firewall-policy-2"
+		group1  = "test-network-firewall-group-1"
+		group2  = "test-network-firewall-group-2"
+	)
+
+	nfw := NetworkFirewallResourcePolicy{
+		Client: mockedNetworkFirewallResourcePolicy{
+			ListFirewallPoliciesOutput: networkfirewall.ListFirewallPoliciesOutput{
+				FirewallPolicies: []*networkfirewall.FirewallPolicyMetadata{
+					{
+						Arn: awsgo.String(policy1),
+					},
+					{
+						Arn: awsgo.String(policy2),
+					},
+				},
+			},
+			ListRuleGroupsOutput: networkfirewall.ListRuleGroupsOutput{
+				RuleGroups: []*networkfirewall.RuleGroupMetadata{
+					{
+						Arn: awsgo.String(group1),
+					},
+					{
+						Arn: awsgo.String(group2),
+					},
+				},
+			},
+			DescribeResourcePolicyOutput: networkfirewall.DescribeResourcePolicyOutput{
+				Policy: aws.String("policy-statements"),
+			},
+		},
+	}
+
+	nfw.BaseAwsResource.Init(nil)
+
+	tests := map[string]struct {
+		configObj config.ResourceType
+		expected  []string
+	}{
+		"emptyFilter": {
+			configObj: config.ResourceType{},
+			expected:  []string{policy1, policy2, group1, group2},
+		},
+		"nameExclusionFilter": {
+			configObj: config.ResourceType{
+				ExcludeRule: config.FilterRule{
+					NamesRegExp: []config.Expression{{
+						RE: *regexp.MustCompile(policy1),
+					}}},
+			},
+			expected: []string{policy2, group1, group2},
+		},
+	}
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			names, err := nfw.getAll(context.Background(), config.Config{
+				NetworkFirewallResourcePolicy: tc.configObj,
+			})
+			require.NoError(t, err)
+			require.Equal(t, tc.expected, aws.StringValueSlice(names))
+		})
+	}
+}
+
+func TestNetworkFirewallResourcePolicy_NukeAll(t *testing.T) {
+
+	t.Parallel()
+
+	ngw := NetworkFirewallResourcePolicy{
+		Client: mockedNetworkFirewallResourcePolicy{
+			DeleteResourcePolicyOutput: networkfirewall.DeleteResourcePolicyOutput{},
+		},
+	}
+
+	err := ngw.nukeAll([]*string{aws.String("test")})
+	require.NoError(t, err)
+}
diff --git a/config/config_test.go b/config/config_test.go
index a26ccaa..75c7bc5 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -107,6 +107,7 @@ func emptyConfig() *Config {
 		ResourceType{FilterRule{}, FilterRule{}, ""},
 		ResourceType{FilterRule{}, FilterRule{}, ""},
 		ResourceType{FilterRule{}, FilterRule{}, ""},
+		ResourceType{FilterRule{}, FilterRule{}, ""},
 	}
 }