Repository: yonahd/kor
Pull request #310 (2024-06-18 10:41:16)

feat: consider ingress and egress rules for networkpolicies
<!-- Please read [CONTRIBUTING.md](CONTRIBUTING.md) for additional information on contributing to this repository! -->

<!--
  !!!! README !!!! Please fill this out.

  Please follow conventional commit naming conventions:

  https://www.conventionalcommits.org/en/v1.0.0/#summary
-->

<!-- A short description of what your PR does and what it solves. -->

## What this PR does / why we need it?

This PR added consideration for discovering second-level unused NetworkPolicies with matching `.spec.podSelector`, but unmatched pods in Ingress/Egress rules.

See https://github.com/yonahd/kor/pull/296#issuecomment-2166571938

```bash
kor version: vdev

  _  _____  ____
 | |/ / _ \|  _ \
 | ' / | | | |_) |
 | . \ |_| |  _ <
 |_|\_\___/|_| \_\

Unused resources in namespace: "default"
+---+---------------+---------------------+---------------------------------------------------------+
| # | RESOURCE TYPE |    RESOURCE NAME    |                         REASON                          |
+---+---------------+---------------------+---------------------------------------------------------+
| 1 | NetworkPolicy | test-network-policy | NetworkPolicy Ingress and Egress rules apply to 0 pods |
+---+---------------+---------------------+---------------------------------------------------------+
```

## PR Checklist

- [ ] This PR adds K8s exceptions (false positives)
- [x] This PR adds new code
- [x] This PR includes tests for new/existing code
- [x] This PR adds docs
<!-- - [ ] This PR does something else -->

## GitHub Issue

Solves #312 

<!-- Notes that may be helpful for anyone reviewing this PR -->

## Notes for your reviewers

Rules with `ipBlock` is considered "used" here.

codecov-commenter: ## [Codecov](https://app.codecov.io/gh/yonahd/kor/pull/310?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Yonah+Dissen) Report
Attention: Patch coverage is `31.08108%` with `51 lines` in your changes missing coverage. Please review.
> Project coverage is 41.00%. Comparing base [(`a2f3629`)](https://app.codecov.io/gh/yonahd/kor/commit/a2f3629e7738e8013ddaa470da5f6cde434ebef3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Yonah+Dissen) to head [(`438aeae`)](https://app.codecov.io/gh/yonahd/kor/commit/438aeae83c0000594665899e99881db515f71da4?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Yonah+Dissen).

| [Files](https://app.codecov.io/gh/yonahd/kor/pull/310?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Yonah+Dissen) | Patch % | Lines |
|---|---|---|
| [pkg/kor/networkpolicies.go](https://app.codecov.io/gh/yonahd/kor/pull/310?src=pr&el=tree&filepath=pkg%2Fkor%2Fnetworkpolicies.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Yonah+Dissen#diff-cGtnL2tvci9uZXR3b3JrcG9saWNpZXMuZ28=) | 28.16% | [47 Missing and 4 partials :warning: ](https://app.codecov.io/gh/yonahd/kor/pull/310?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Yonah+Dissen) |

:exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #310      +/-   ##
==========================================
- Coverage   41.38%   41.00%   -0.39%     
==========================================
  Files          61       61              
  Lines        3204     3268      +64     
==========================================
+ Hits         1326     1340      +14     
- Misses       1668     1715      +47     
- Partials      210      213       +3     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/yonahd/kor/pull/310?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Yonah+Dissen).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Yonah+Dissen).

doronkg: @tthvo I've created a new issue to address this enhancement - #312, please update the description and reference it.
Let us know when the draft is ready to review 💪 
tthvo: @doronkg @yonahd I think this is ready now for review. I have run some extensive tests but might miss some cases. Please let me know what you think :D
yonahd: @tthvo Sorry for the late review.
tthvo: > @tthvo Sorry for the late review.

All good! I addressed the reviews in the latest commit :D

Patch:
diff --git a/README.md b/README.md
index 80fdee8..748a2da 100644
--- a/README.md
+++ b/README.md
@@ -177,7 +177,7 @@ kor [subcommand] --help
 | ReplicaSets     | replicaSets that specify replicas to 0 and has already completed it's work                                                                                                                                                        |
 | DaemonSets      | DaemonSets not scheduled on any nodes                                                                                                                                                                                             |
 | StorageClasses  | StorageClasses not used by any PVs/PVCs                                                                                                                                                                                           |
-| NetworkPolicies  | NetworkPolicies with no Pods selected                                                                                                                                                                                           |
+| NetworkPolicies  | NetworkPolicies with no Pods selected by podSelector or Ingress/Egress rules                                                                                                                                                                                           |
 
 ### Deleting Unused resources
 
diff --git a/charts/kor/Chart.yaml b/charts/kor/Chart.yaml
index 18a2797..fffea60 100644
--- a/charts/kor/Chart.yaml
+++ b/charts/kor/Chart.yaml
@@ -2,8 +2,8 @@ apiVersion: v2
 name: kor
 description: A Kubernetes Helm Chart to discover orphaned resources using kor
 type: application
-version: 0.1.9
-appVersion: "0.5.1"
+version: 0.1.10
+appVersion: "0.5.2"
 maintainers:
   - name: "yonahd"
     url: "https://github.com/yonahd/kor"
diff --git a/charts/kor/README.md b/charts/kor/README.md
index d329280..7eb79fd 100644
--- a/charts/kor/README.md
+++ b/charts/kor/README.md
@@ -1,6 +1,6 @@
 # kor
 
-![Version: 0.1.9](https://img.shields.io/badge/Version-0.1.9-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.5.1](https://img.shields.io/badge/AppVersion-0.5.1-informational?style=flat-square)
+![Version: 0.1.10](https://img.shields.io/badge/Version-0.1.10-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.5.2](https://img.shields.io/badge/AppVersion-0.5.2-informational?style=flat-square)
 
 A Kubernetes Helm Chart to discover orphaned resources using kor
 
@@ -61,4 +61,4 @@ A Kubernetes Helm Chart to discover orphaned resources using kor
 | serviceAccount.name | string | `""` | If not set and create is true, a name is generated using the fullname template |
 
 ----------------------------------------------
-Autogenerated from chart metadata using [helm-docs v1.13.1](https://github.com/norwoodj/helm-docs/releases/v1.13.1)
+Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2)
diff --git a/pkg/kor/networkpolicies.go b/pkg/kor/networkpolicies.go
index 03a89e1..560a09b 100644
--- a/pkg/kor/networkpolicies.go
+++ b/pkg/kor/networkpolicies.go
@@ -6,13 +6,115 @@ import (
 	"encoding/json"
 	"fmt"
 	"os"
+	"slices"
 
+	v1 "k8s.io/api/core/v1"
+	networkingv1 "k8s.io/api/networking/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/kubernetes"
 
 	"github.com/yonahd/kor/pkg/filters"
 )
 
+const (
+	unusedLabelReason         = "Marked with unused label"
+	noPodAppliedReason        = "NetworkPolicy applies to 0 pods"
+	noPodAppliedByRulesReason = "NetworkPolicy Ingress and Egress rules apply to 0 pods"
+)
+
+func retrievePodsForSelector(clientset kubernetes.Interface, namespace string, selector *metav1.LabelSelector) ([]v1.Pod, error) {
+	labelSelector, err := metav1.LabelSelectorAsSelector(selector)
+	if err != nil {
+		return nil, err
+	}
+	podList, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{
+		LabelSelector: labelSelector.String(),
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return podList.Items, nil
+}
+
+func isAnyPodMatchedInSources(clientset kubernetes.Interface, sources []networkingv1.NetworkPolicyPeer) (bool, error) {
+	// If this field is empty or missing, this rule matches all pods
+	if len(sources) == 0 {
+		return true, nil
+	}
+
+	for _, netpolPeer := range sources {
+		// If ipBlock is specified, assume the source is in use
+		if netpolPeer.IPBlock != nil {
+			return true, nil
+		}
+
+		labelSelector, err := metav1.LabelSelectorAsSelector(netpolPeer.NamespaceSelector)
+		if err != nil {
+			return false, err
+		}
+
+		nsList, err := clientset.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{
+			LabelSelector: labelSelector.String(),
+		})
+		if err != nil {
+			return false, err
+		}
+
+		for _, ns := range nsList.Items {
+			podList, err := retrievePodsForSelector(clientset, ns.Name, netpolPeer.PodSelector)
+			if err != nil {
+				return false, err
+			}
+
+			if len(podList) > 0 {
+				return true, nil
+			}
+		}
+	}
+
+	return false, nil
+}
+
+func isAnyIngressRuleUsed(clientset kubernetes.Interface, netpol networkingv1.NetworkPolicy) (bool, error) {
+	// Deny all ingress traffic
+	if len(netpol.Spec.Ingress) == 0 && slices.Contains(netpol.Spec.PolicyTypes, networkingv1.PolicyTypeIngress) {
+		return true, nil
+	}
+	for _, ingressRule := range netpol.Spec.Ingress {
+		podsMatched, err := isAnyPodMatchedInSources(clientset, ingressRule.From)
+		if err != nil {
+			return false, err
+		}
+
+		if podsMatched {
+			return true, nil
+		}
+	}
+
+	return false, nil
+}
+
+func isAnyEgressRuleUsed(clientset kubernetes.Interface, netpol networkingv1.NetworkPolicy) (bool, error) {
+	// Deny all egress traffic
+	if len(netpol.Spec.Egress) == 0 && slices.Contains(netpol.Spec.PolicyTypes, networkingv1.PolicyTypeEgress) {
+		return true, nil
+	}
+
+	for _, egressRule := range netpol.Spec.Egress {
+		podsMatched, err := isAnyPodMatchedInSources(clientset, egressRule.To)
+		if err != nil {
+			return false, err
+		}
+
+		if podsMatched {
+			return true, nil
+		}
+	}
+
+	return false, nil
+}
+
 func processNamespaceNetworkPolicies(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ([]ResourceInfo, error) {
 	netpolList, err := clientset.NetworkingV1().NetworkPolicies(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: filterOpts.IncludeLabels})
 	if err != nil {
@@ -27,27 +129,33 @@ func processNamespaceNetworkPolicies(clientset kubernetes.Interface, namespace s
 		}
 
 		if netpol.Labels["kor/used"] == "false" {
-			reason := "Marked with unused label"
-			unusedNetpols = append(unusedNetpols, ResourceInfo{Name: netpol.Name, Reason: reason})
+			unusedNetpols = append(unusedNetpols, ResourceInfo{Name: netpol.Name, Reason: unusedLabelReason})
 			continue
 		}
 
-		// retrieve pods selected by the NetworkPolicy
-		labelSelector, err := metav1.LabelSelectorAsSelector(&netpol.Spec.PodSelector)
+		pods, err := retrievePodsForSelector(clientset, namespace, &netpol.Spec.PodSelector)
 		if err != nil {
 			return nil, err
 		}
-		podList, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{
-			LabelSelector: labelSelector.String(),
-		})
-		if err != nil {
+
+		if len(pods) == 0 {
+			unusedNetpols = append(unusedNetpols, ResourceInfo{Name: netpol.Name, Reason: noPodAppliedReason})
+			continue
+		}
+
+		if used, err := isAnyIngressRuleUsed(clientset, netpol); err != nil {
 			return nil, err
+		} else if used {
+			continue
 		}
 
-		if len(podList.Items) == 0 {
-			reason := "NetworkPolicy selects no pods"
-			unusedNetpols = append(unusedNetpols, ResourceInfo{Name: netpol.Name, Reason: reason})
+		if used, err := isAnyEgressRuleUsed(clientset, netpol); err != nil {
+			return nil, err
+		} else if used {
+			continue
 		}
+
+		unusedNetpols = append(unusedNetpols, ResourceInfo{Name: netpol.Name, Reason: noPodAppliedByRulesReason})
 	}
 
 	return unusedNetpols, nil


Test patch:
diff --git a/pkg/kor/create_test_resources.go b/pkg/kor/create_test_resources.go
index 9781090..bfcd22f 100644
--- a/pkg/kor/create_test_resources.go
+++ b/pkg/kor/create_test_resources.go
@@ -399,7 +399,14 @@ func CreateTestUnstructered(kind, apiVersion, namespace, name string) *unstructu
 	}
 }
 
-func CreateTestNetworkPolicy(name, namespace string, podSelector v1.LabelSelector, labels map[string]string) *networkingv1.NetworkPolicy {
+func CreateTestNetworkPolicy(name, namespace string, labels map[string]string, podSelector v1.LabelSelector, ingress []networkingv1.NetworkPolicyIngressRule, egress []networkingv1.NetworkPolicyEgressRule) *networkingv1.NetworkPolicy {
+	policies := []networkingv1.PolicyType{
+		networkingv1.PolicyTypeIngress,
+	}
+	if len(egress) > 0 {
+		policies = append(policies, networkingv1.PolicyTypeEgress)
+	}
+
 	return &networkingv1.NetworkPolicy{
 		ObjectMeta: v1.ObjectMeta{
 			Name:      name,
@@ -408,6 +415,9 @@ func CreateTestNetworkPolicy(name, namespace string, podSelector v1.LabelSelecto
 		},
 		Spec: networkingv1.NetworkPolicySpec{
 			PodSelector: podSelector,
+			PolicyTypes: policies,
+			Ingress:     ingress,
+			Egress:      egress,
 		},
 	}
 }
diff --git a/pkg/kor/networkpolicies_test.go b/pkg/kor/networkpolicies_test.go
index 27ade65..d689a0b 100644
--- a/pkg/kor/networkpolicies_test.go
+++ b/pkg/kor/networkpolicies_test.go
@@ -19,50 +19,100 @@ import (
 func createTestNetworkPolicies(t *testing.T) *fake.Clientset {
 	clientset := fake.NewSimpleClientset()
 
-	_, err := clientset.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{
-		ObjectMeta: v1.ObjectMeta{Name: testNamespace},
-	}, v1.CreateOptions{})
+	testNamespace2 := "another-namespace"
+	namespaces := []string{testNamespace, testNamespace2}
 
-	if err != nil {
-		t.Fatalf("Error creating namespace %s: %v", testNamespace, err)
+	for _, ns := range namespaces {
+		_, err := clientset.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{
+			ObjectMeta: v1.ObjectMeta{Name: ns},
+		}, v1.CreateOptions{})
+
+		if err != nil {
+			t.Fatalf("Error creating namespace %s: %v", ns, err)
+		}
 	}
 
-	podLabels := map[string]string{
+	podLabels1 := map[string]string{
 		"app.kubernetes.io/name":    "my-app",
 		"app.kubernetes.io/version": "v1",
 		"product.my-org/name":       "my-app",
 	}
-	noMatchLabels := map[string]string{"app.kubernetes.io/version": "v2"}
+	podLabels2 := map[string]string{"app.kubernetes.io/version": "v2"}
 
 	pods := []*corev1.Pod{
-		CreateTestPod(testNamespace, "pod-1", "", nil, podLabels),
+		CreateTestPod(testNamespace, "pod-1", "", nil, podLabels1),
 		CreateTestPod(testNamespace, "pod-2", "", nil, AppLabels),
+		CreateTestPod(testNamespace2, "pod-1", "", nil, podLabels2),
+		CreateTestPod(testNamespace2, "pod-2", "", nil, AppLabels),
 	}
 
 	for _, pod := range pods {
-		_, err = clientset.CoreV1().Pods(testNamespace).Create(context.TODO(), pod, v1.CreateOptions{})
+		_, err := clientset.CoreV1().Pods(pod.Namespace).Create(context.TODO(), pod, v1.CreateOptions{})
 		if err != nil {
 			t.Fatalf("Error creating fake pod: %v", err)
 		}
 	}
 
 	netpols := []*networkingv1.NetworkPolicy{
-		// all pods are selected
-		CreateTestNetworkPolicy("netpol-1", testNamespace, v1.LabelSelector{}, AppLabels),
-		CreateTestNetworkPolicy("netpol-2", testNamespace, v1.LabelSelector{}, UsedLabels),
-		CreateTestNetworkPolicy("netpol-3", testNamespace, v1.LabelSelector{}, UnusedLabels),
-		// some pods are selected
-		CreateTestNetworkPolicy("netpol-4", testNamespace, *v1.SetAsLabelSelector(podLabels), AppLabels),
-		CreateTestNetworkPolicy("netpol-5", testNamespace, *v1.SetAsLabelSelector(podLabels), UnusedLabels),
-		CreateTestNetworkPolicy("netpol-6", testNamespace, *v1.SetAsLabelSelector(podLabels), UsedLabels),
+		// with kor labels
+		CreateTestNetworkPolicy("netpol-1", testNamespace, UsedLabels, v1.LabelSelector{}, nil, nil),
+		CreateTestNetworkPolicy("netpol-2", testNamespace, UnusedLabels, v1.LabelSelector{}, nil, nil),
+
+		// with pod selectors
 		// no pods are selected
-		CreateTestNetworkPolicy("netpol-7", testNamespace, *v1.SetAsLabelSelector(noMatchLabels), AppLabels),
-		CreateTestNetworkPolicy("netpol-8", testNamespace, *v1.SetAsLabelSelector(noMatchLabels), UnusedLabels),
-		CreateTestNetworkPolicy("netpol-9", testNamespace, *v1.SetAsLabelSelector(noMatchLabels), UsedLabels),
+		CreateTestNetworkPolicy("netpol-3", testNamespace, AppLabels, *v1.SetAsLabelSelector(podLabels2), nil, nil),
+
+		// with ingress/egress rules
+		// deny-all ingress
+		CreateTestNetworkPolicy("netpol-4", testNamespace, AppLabels, *v1.SetAsLabelSelector(podLabels1), nil, nil),
+		// allow-all ingress
+		CreateTestNetworkPolicy("netpol-5", testNamespace, AppLabels, *v1.SetAsLabelSelector(podLabels1), []networkingv1.NetworkPolicyIngressRule{{}}, nil),
+		// allow ingress to some pods
+		CreateTestNetworkPolicy("netpol-6", testNamespace, AppLabels, *v1.SetAsLabelSelector(podLabels1), []networkingv1.NetworkPolicyIngressRule{{
+			From: []networkingv1.NetworkPolicyPeer{
+				{
+					PodSelector: &v1.LabelSelector{
+						MatchLabels: podLabels2,
+					},
+				},
+			},
+		}}, nil),
+		// ingress matches 0 pods
+		CreateTestNetworkPolicy("netpol-7", testNamespace, AppLabels, *v1.SetAsLabelSelector(podLabels1), []networkingv1.NetworkPolicyIngressRule{{
+			From: []networkingv1.NetworkPolicyPeer{
+				{
+					PodSelector: &v1.LabelSelector{
+						MatchLabels: map[string]string{"product.my-org/name": "unknown"},
+					},
+				},
+			},
+		}}, nil),
+		// with ipBlock
+		CreateTestNetworkPolicy("netpol-8", testNamespace, AppLabels, *v1.SetAsLabelSelector(podLabels1), []networkingv1.NetworkPolicyIngressRule{{
+			From: []networkingv1.NetworkPolicyPeer{
+				{
+					IPBlock: &networkingv1.IPBlock{
+						CIDR: "172.17.0.0/16",
+					},
+				},
+			},
+		}}, nil),
 	}
 
+	netpol9 := CreateTestNetworkPolicy("netpol-9", testNamespace, AppLabels, *v1.SetAsLabelSelector(podLabels1), nil, []networkingv1.NetworkPolicyEgressRule{{
+		To: []networkingv1.NetworkPolicyPeer{
+			{
+				PodSelector: &v1.LabelSelector{
+					MatchLabels: map[string]string{"product.my-org/name": "unknown"},
+				},
+			},
+		},
+	}}) // egress only - matches 0 pods
+	netpol9.Spec.PolicyTypes = []networkingv1.PolicyType{networkingv1.PolicyTypeEgress}
+	netpols = append(netpols, netpol9)
+
 	for _, netpol := range netpols {
-		_, err = clientset.NetworkingV1().NetworkPolicies(netpol.Namespace).Create(context.TODO(), netpol, v1.CreateOptions{})
+		_, err := clientset.NetworkingV1().NetworkPolicies(netpol.Namespace).Create(context.TODO(), netpol, v1.CreateOptions{})
 		if err != nil {
 			t.Fatalf("Error creating fake networkpolicy: %v", err)
 		}
@@ -71,6 +121,87 @@ func createTestNetworkPolicies(t *testing.T) *fake.Clientset {
 	return clientset
 }
 
+func TestRetrievePodsForSelector(t *testing.T) {
+	clientset := createTestNetworkPolicies(t)
+
+	selector := &v1.LabelSelector{
+		MatchLabels: map[string]string{
+			"app.kubernetes.io/version": "v1",
+		},
+	}
+	pods, err := retrievePodsForSelector(clientset, testNamespace, selector)
+	if err != nil {
+		t.Errorf("Error retrieving pods for selector %v: %v", selector, err)
+	}
+
+	expectedPods := []string{
+		"pod-1",
+	}
+
+	if len(pods) != len(expectedPods) {
+		t.Errorf("Expected %d pods, got %d", len(expectedPods), len(pods))
+	}
+
+	for i, pod := range pods {
+		if pod.Name != expectedPods[i] {
+			t.Errorf("Expected pod %s, got %v", expectedPods[i], pod)
+		}
+	}
+}
+
+func TestIsAnyPodMatchedInSources(t *testing.T) {
+	clientset := createTestNetworkPolicies(t)
+
+	sources := []networkingv1.NetworkPolicyPeer{
+		{
+			PodSelector: &v1.LabelSelector{
+				MatchLabels: map[string]string{
+					"app.kubernetes.io/version": "v1",
+				},
+			},
+		},
+	}
+
+	matched, err := isAnyPodMatchedInSources(clientset, sources)
+	if err != nil {
+		t.Errorf("Error checking if sources match any pods: %v", err)
+	}
+
+	if !matched {
+		t.Error("Expected matching pods, got none")
+	}
+}
+
+func TestIsAnyIngressRuleUsed(t *testing.T) {
+	clientset := createTestNetworkPolicies(t)
+
+	netpol := CreateTestNetworkPolicy("netpol-0", testNamespace, AppLabels, v1.LabelSelector{}, nil, nil)
+
+	used, err := isAnyIngressRuleUsed(clientset, *netpol)
+	if err != nil {
+		t.Errorf("Error checking if any ingress rule is used: %v", err)
+	}
+
+	if !used {
+		t.Error("Expected ingress rules in use, got none")
+	}
+}
+
+func TestIsAnyEgressRuleUsed(t *testing.T) {
+	clientset := createTestNetworkPolicies(t)
+
+	netpol := CreateTestNetworkPolicy("netpol-0", testNamespace, AppLabels, v1.LabelSelector{}, nil, nil)
+
+	used, err := isAnyEgressRuleUsed(clientset, *netpol)
+	if err != nil {
+		t.Errorf("Error checking if any egress rule is used: %v", err)
+	}
+
+	if used {
+		t.Error("Expected ingress rules not in use, got used rules")
+	}
+}
+
 func TestProcessNamespaceNetworkPolicies(t *testing.T) {
 	clientset := createTestNetworkPolicies(t)
 
@@ -80,10 +211,10 @@ func TestProcessNamespaceNetworkPolicies(t *testing.T) {
 	}
 
 	expectedUnusedNetpols := []string{
+		"netpol-2",
 		"netpol-3",
-		"netpol-5",
 		"netpol-7",
-		"netpol-8",
+		"netpol-9",
 	}
 
 	if len(unusedNetpols) != len(expectedUnusedNetpols) {
@@ -117,10 +248,10 @@ func TestGetUnusedNetworkPolicies(t *testing.T) {
 	expectedOutput := map[string]map[string][]string{
 		testNamespace: {
 			"NetworkPolicy": []string{
+				"netpol-2",
 				"netpol-3",
-				"netpol-5",
 				"netpol-7",
-				"netpol-8",
+				"netpol-9",
 			},
 		},
 	}