Repository: operator-framework/operator-controller
Pull request #2013 (2025-06-05 14:56:43)

✨OPRUN-3873: Add e2e tests for NetworkPolicies
<!--
Please prefix the title of this PR with one of the following icons:

    * ⚠ (:warning:, major/breaking change)
    * ✨ (:sparkles:, minor/compatible change)
    * 🐛 (:bug:, patch/bug fix)
    * 📖 (:book:, docs)
    * 🌱 (:seedling:, other)

-->

# Description

This PR introduces a new e2e test to continuously validate the NetworkPolicy objects for the catalogd and operator-controller components. 

The new test, `TestNetworkPolicyJustifications`, achieves the following:

* Documents NetworkPolicies deployed: A registry of all allowed network policies (`allowedNetworkPolicies`) is defined using the [RFC: OLMv1 Static Network Policies](https://docs.google.com/document/d/1bHEEWzA65u-kjJFQRUY1iBuMIIM1HbPy4MeDLX4NI3o/edit?tab=t.0#heading=h.x3tfh25grvnv). Every rule within these policies is explicitly registered in the registry with a clear, written justification of a minimum length. This ensures that every rule's purpose is documented and understood.

* Prevents Regressions: The test validates that the NetworkPolicy objects deployed in the cluster exactly match the definitions in a registry (`allowedNetworkPolicies`). This prevents undocumented or accidental changes to our network rules.

* Improves Security Posture: Enforces thoughtful consideration for any change to existing network rules, improving overall security posture. Also ensures that other, unrelated network policies won't inadvertently break existing functionalities.


## Reviewer Checklist

- [ ] API Go Documentation
- [ ] Tests: Unit Tests (and E2E Tests, if appropriate)
- [ ] Comprehensive Commit Messages
- [ ] Links to related GitHub Issue(s)


openshift-ci[bot]: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by:
**Once this PR has been reviewed and has the lgtm label**, please assign [perdasilva](https://github.com/perdasilva) for approval. For more information see [the Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=operator-framework%2Foperator-controller).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/operator-framework/operator-controller/blob/main/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["perdasilva"]} -->
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/operator-framework/operator-controller/pull/2013?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=operator-framework) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 69.17%. Comparing base [(`8f81c23`)](https://app.codecov.io/gh/operator-framework/operator-controller/commit/8f81c2338489188eae3a00f10bc6bdee89bc22d1?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=operator-framework) to head [(`e8d954d`)](https://app.codecov.io/gh/operator-framework/operator-controller/commit/e8d954d1aca21e8170d00c0e2b1e627f0820735b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=operator-framework).

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


```diff
@@           Coverage Diff           @@
##             main    #2013   +/-   ##
=======================================
  Coverage   69.17%   69.17%           
=======================================
  Files          79       79           
  Lines        7037     7037           
=======================================
  Hits         4868     4868           
  Misses       1887     1887           
  Partials      282      282           
```

| [Flag](https://app.codecov.io/gh/operator-framework/operator-controller/pull/2013/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=operator-framework) | Coverage Δ | |
|---|---|---|
| [e2e](https://app.codecov.io/gh/operator-framework/operator-controller/pull/2013/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=operator-framework) | `43.00% <ø> (ø)` | |
| [unit](https://app.codecov.io/gh/operator-framework/operator-controller/pull/2013/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=operator-framework) | `60.05% <ø> (ø)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=operator-framework#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/operator-framework/operator-controller/pull/2013?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=operator-framework).   
: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=operator-framework).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
anik120: @joelanford @perdasilva regarding the discussion about possibly switching this to a unit test, I see the Quickstart target in the Makefile, and ack the discussion about using steps from that target (separated out) to generate the manifests and before starting the unit tests, and then consuming the generated manifests from the unit test. 

I don't see a natural home for the unit test though. Maybe that's actually a good signal that this should be a e2e test instead. 

Thoughts?
anik120: @camilamacedo86 PR #2025 gives me the manifests locally that I can consume and write the test with. 

However the question of "which package is a natural home for that test, and if we cannot think of a natural home package for that test is that a strong signal that this is better suited as an e2e test", still remains. 

Maybe we wait a little to see if anyone has a better idea, if not I'm happy with this PR and we can merge it in 👍🏽 
tmshort: Make sure you rebase...

Patch:
diff --git a/internal/operator-controller/rukpak/preflights/crdupgradesafety/checks.go b/internal/operator-controller/rukpak/preflights/crdupgradesafety/checks.go
index 669f65e..61d8b55 100644
--- a/internal/operator-controller/rukpak/preflights/crdupgradesafety/checks.go
+++ b/internal/operator-controller/rukpak/preflights/crdupgradesafety/checks.go
@@ -242,3 +242,13 @@ func Type(diff FieldDiff) (bool, error) {
 
 	return isHandled(diff, reset), err
 }
+
+// Description changes are considered safe and non-breaking.
+func Description(diff FieldDiff) (bool, error) {
+	reset := func(diff FieldDiff) FieldDiff {
+		diff.Old.Description = ""
+		diff.New.Description = ""
+		return diff
+	}
+	return isHandled(diff, reset), nil
+}
diff --git a/internal/operator-controller/rukpak/preflights/crdupgradesafety/crdupgradesafety.go b/internal/operator-controller/rukpak/preflights/crdupgradesafety/crdupgradesafety.go
index 6bc177c..0904bf4 100644
--- a/internal/operator-controller/rukpak/preflights/crdupgradesafety/crdupgradesafety.go
+++ b/internal/operator-controller/rukpak/preflights/crdupgradesafety/crdupgradesafety.go
@@ -31,6 +31,7 @@ type Preflight struct {
 
 func NewPreflight(crdCli apiextensionsv1client.CustomResourceDefinitionInterface, opts ...Option) *Preflight {
 	changeValidations := []ChangeValidation{
+		Description,
 		Enum,
 		Required,
 		Maximum,


Test patch:
diff --git a/internal/operator-controller/rukpak/preflights/crdupgradesafety/checks_test.go b/internal/operator-controller/rukpak/preflights/crdupgradesafety/checks_test.go
index 36618b5..ebceed8 100644
--- a/internal/operator-controller/rukpak/preflights/crdupgradesafety/checks_test.go
+++ b/internal/operator-controller/rukpak/preflights/crdupgradesafety/checks_test.go
@@ -904,3 +904,105 @@ func TestType(t *testing.T) {
 		})
 	}
 }
+
+func TestDescription(t *testing.T) {
+	for _, tc := range []testcase{
+		{
+			name: "no diff, no error, handled",
+			diff: FieldDiff{
+				Old: &apiextensionsv1.JSONSchemaProps{
+					Description: "some field",
+				},
+				New: &apiextensionsv1.JSONSchemaProps{
+					Description: "some field",
+				},
+			},
+			err:     nil,
+			handled: true,
+		},
+		{
+			name: "description changed, no error, handled",
+			diff: FieldDiff{
+				Old: &apiextensionsv1.JSONSchemaProps{
+					Description: "old description",
+				},
+				New: &apiextensionsv1.JSONSchemaProps{
+					Description: "new description",
+				},
+			},
+			err:     nil,
+			handled: true,
+		},
+		{
+			name: "description added, no error, handled",
+			diff: FieldDiff{
+				Old: &apiextensionsv1.JSONSchemaProps{},
+				New: &apiextensionsv1.JSONSchemaProps{
+					Description: "a new description was added",
+				},
+			},
+			err:     nil,
+			handled: true,
+		},
+		{
+			name: "description removed, no error, handled",
+			diff: FieldDiff{
+				Old: &apiextensionsv1.JSONSchemaProps{
+					Description: "this description will be removed",
+				},
+				New: &apiextensionsv1.JSONSchemaProps{},
+			},
+			err:     nil,
+			handled: true,
+		},
+		{
+			name: "different field changed, no error, not handled",
+			diff: FieldDiff{
+				Old: &apiextensionsv1.JSONSchemaProps{
+					ID: "foo",
+				},
+				New: &apiextensionsv1.JSONSchemaProps{
+					ID: "bar",
+				},
+			},
+			err:     nil,
+			handled: false,
+		},
+		{
+			name: "different field changed with description, no error, not handled",
+			diff: FieldDiff{
+				Old: &apiextensionsv1.JSONSchemaProps{
+					ID:          "foo",
+					Description: "description",
+				},
+				New: &apiextensionsv1.JSONSchemaProps{
+					ID:          "bar",
+					Description: "description",
+				},
+			},
+			err:     nil,
+			handled: false,
+		},
+		{
+			name: "description and ID changed, no error, not handled",
+			diff: FieldDiff{
+				Old: &apiextensionsv1.JSONSchemaProps{
+					ID:          "foo",
+					Description: "old description",
+				},
+				New: &apiextensionsv1.JSONSchemaProps{
+					ID:          "bar",
+					Description: "new description",
+				},
+			},
+			err:     nil,
+			handled: false,
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			handled, err := Description(tc.diff)
+			require.Equal(t, tc.err, err)
+			require.Equal(t, tc.handled, handled)
+		})
+	}
+}
diff --git a/test/e2e/network_policy_test.go b/test/e2e/network_policy_test.go
new file mode 100644
index 0000000..d4bf334
--- /dev/null
+++ b/test/e2e/network_policy_test.go
@@ -0,0 +1,337 @@
+package e2e
+
+import (
+	"context"
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	corev1 "k8s.io/api/core/v1"
+	networkingv1 "k8s.io/api/networking/v1"
+	"k8s.io/apimachinery/pkg/api/equality"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/intstr"
+	"k8s.io/utils/ptr"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/operator-framework/operator-controller/test/utils"
+)
+
+const (
+	minJustificationLength        = 40
+	catalogdManagerSelector       = "control-plane=catalogd-controller-manager"
+	operatorManagerSelector       = "control-plane=operator-controller-controller-manager"
+	catalogdMetricsPort           = 7443
+	catalogdWebhookPort           = 9443
+	catalogServerPort             = 8443
+	operatorControllerMetricsPort = 8443
+)
+
+type portWithJustification struct {
+	port          []networkingv1.NetworkPolicyPort
+	justification string
+}
+
+// ingressRule defines a k8s IngressRule, along with a justification.
+type ingressRule struct {
+	ports []portWithJustification
+	from  []networkingv1.NetworkPolicyPeer
+}
+
+// egressRule defines a k8s egressRule, along with a justification.
+type egressRule struct {
+	ports []portWithJustification
+	to    []networkingv1.NetworkPolicyPeer
+}
+
+// AllowedPolicyDefinition defines the expected structure and justifications for a NetworkPolicy.
+type allowedPolicyDefinition struct {
+	selector                    metav1.LabelSelector
+	policyTypes                 []networkingv1.PolicyType
+	ingressRule                 ingressRule
+	egressRule                  egressRule
+	denyAllIngressJustification string // Justification if Ingress is in PolicyTypes and IngressRules is empty
+	denyAllEgressJustification  string // Justification if Egress is in PolicyTypes and EgressRules is empty
+}
+
+// Ref: https://docs.google.com/document/d/1bHEEWzA65u-kjJFQRUY1iBuMIIM1HbPy4MeDLX4NI3o/edit?usp=sharing
+var allowedNetworkPolicies = map[string]allowedPolicyDefinition{
+	"catalogd-controller-manager": {
+		selector:    metav1.LabelSelector{MatchLabels: map[string]string{"control-plane": "catalogd-controller-manager"}},
+		policyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress},
+		ingressRule: ingressRule{
+			ports: []portWithJustification{
+				{
+					port:          []networkingv1.NetworkPolicyPort{{Protocol: ptr.To(corev1.ProtocolTCP), Port: &intstr.IntOrString{Type: intstr.Int, IntVal: catalogdMetricsPort}}},
+					justification: "Allows Prometheus to scrape metrics from catalogd, which is essential for monitoring its performance and health.",
+				},
+				{
+					port:          []networkingv1.NetworkPolicyPort{{Protocol: ptr.To(corev1.ProtocolTCP), Port: &intstr.IntOrString{Type: intstr.Int, IntVal: catalogdWebhookPort}}},
+					justification: "Permits Kubernetes API server to reach catalogd's mutating admission webhook, ensuring integrity of catalog resources.",
+				},
+				{
+					port:          []networkingv1.NetworkPolicyPort{{Protocol: ptr.To(corev1.ProtocolTCP), Port: &intstr.IntOrString{Type: intstr.Int, IntVal: catalogServerPort}}},
+					justification: "Enables clients (eg. operator-controller) to query catalog metadata from catalogd, which is a core function for bundle resolution and operator discovery.",
+				},
+			},
+		},
+		egressRule: egressRule{
+			ports: []portWithJustification{
+				{
+					port:          nil, // Empty Ports means allow all egress
+					justification: "Permits catalogd to fetch catalog images from arbitrary container registries and communicate with the Kubernetes API server for its operational needs.",
+				},
+			},
+		},
+	},
+	"operator-controller-controller-manager": {
+		selector:    metav1.LabelSelector{MatchLabels: map[string]string{"control-plane": "operator-controller-controller-manager"}},
+		policyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress},
+		ingressRule: ingressRule{
+			ports: []portWithJustification{
+				{
+					port:          []networkingv1.NetworkPolicyPort{{Protocol: ptr.To(corev1.ProtocolTCP), Port: &intstr.IntOrString{Type: intstr.Int, IntVal: operatorControllerMetricsPort}}},
+					justification: "Allows Prometheus to scrape metrics from operator-controller, which is crucial for monitoring its activity, reconciliations, and overall health.",
+				},
+			},
+		},
+		egressRule: egressRule{
+			ports: []portWithJustification{
+				{
+					port:          nil, // Empty Ports means allow all egress
+					justification: "Enables operator-controller to pull bundle images from arbitrary image registries, connect to catalogd's HTTPS server for metadata, and interact with the Kubernetes API server.",
+				},
+			},
+		},
+	},
+	"default-deny-all-traffic": {
+		selector:    metav1.LabelSelector{}, // Empty selector, matches all pods
+		policyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress},
+		// No IngressRules means deny all ingress if PolicyTypeIngress is present
+		// No EgressRules means deny all egress if PolicyTypeEgress is present
+		denyAllIngressJustification: "Denies all ingress traffic to pods selected by this policy by default, unless explicitly allowed by other policy rules, ensuring a baseline secure posture.",
+		denyAllEgressJustification:  "Denies all egress traffic from pods selected by this policy by default, unless explicitly allowed by other policy rules, minimizing potential exfiltration paths.",
+	},
+}
+
+func TestNetworkPolicyJustifications(t *testing.T) {
+	ctx := context.Background()
+
+	// Validate justifications have min length in the allowedNetworkPolicies definition
+	for name, policyDef := range allowedNetworkPolicies {
+		for i, pwj := range policyDef.ingressRule.ports {
+			assert.GreaterOrEqualf(t, len(pwj.justification), minJustificationLength,
+				"Justification for ingress PortWithJustification entry %d in policy %q is too short: %q", i, name, pwj.justification)
+		}
+		for i, pwj := range policyDef.egressRule.ports { // Corrected variable name from 'rule' to 'pwj'
+			assert.GreaterOrEqualf(t, len(pwj.justification), minJustificationLength,
+				"Justification for egress PortWithJustification entry %d in policy %q is too short: %q", i, name, pwj.justification)
+		}
+		if policyDef.denyAllIngressJustification != "" {
+			assert.GreaterOrEqualf(t, len(policyDef.denyAllIngressJustification), minJustificationLength,
+				"DenyAllIngressJustification for policy %q is too short: %q", name, policyDef.denyAllIngressJustification)
+		}
+		if policyDef.denyAllEgressJustification != "" {
+			assert.GreaterOrEqualf(t, len(policyDef.denyAllEgressJustification), minJustificationLength,
+				"DenyAllEgressJustification for policy %q is too short: %q", name, policyDef.denyAllEgressJustification)
+		}
+	}
+
+	clientForComponent := utils.FindK8sClient(t)
+	componentNamespace := getComponentNamespace(t, clientForComponent, operatorManagerSelector)
+	clusterPolicies := &networkingv1.NetworkPolicyList{}
+	err := c.List(ctx, clusterPolicies, client.InNamespace(componentNamespace))
+	require.NoError(t, err, "Failed to list NetworkPolicies in namespace %q", componentNamespace)
+
+	validatedRegistryPolicies := make(map[string]bool)
+
+	for _, policy := range clusterPolicies.Items {
+		t.Run(fmt.Sprintf("Policy_%s", strings.ReplaceAll(policy.Name, "-", "_")), func(t *testing.T) {
+			expectedPolicy, found := allowedNetworkPolicies[policy.Name]
+			require.Truef(t, found, "NetworkPolicy %q found in cluster but not in allowed registry. Namespace: %s", policy.Name, policy.Namespace)
+			validatedRegistryPolicies[policy.Name] = true
+
+			// 1. Compare PodSelector
+			assert.True(t, equality.Semantic.DeepEqual(expectedPolicy.selector, policy.Spec.PodSelector),
+				"PodSelector mismatch for policy %q. Expected: %+v, Got: %+v", policy.Name, expectedPolicy.selector, policy.Spec.PodSelector)
+
+			// 2. Compare PolicyTypes
+			require.ElementsMatchf(t, expectedPolicy.policyTypes, policy.Spec.PolicyTypes,
+				"PolicyTypes mismatch for policy %q.", policy.Name)
+
+			// 3. Validate Ingress Rules
+			hasIngressPolicyType := false
+			for _, pt := range policy.Spec.PolicyTypes {
+				if pt == networkingv1.PolicyTypeIngress {
+					hasIngressPolicyType = true
+					break
+				}
+			}
+
+			if hasIngressPolicyType {
+				switch len(policy.Spec.Ingress) {
+				case 0:
+					validateDenyAllIngress(t, policy.Name, expectedPolicy)
+				case 1:
+					validateSingleIngressRule(t, policy.Name, policy.Spec.Ingress[0], expectedPolicy)
+				default:
+					assert.Failf(t, "Policy %q in cluster has %d ingress rules. Allowed definition supports at most 1 explicit ingress rule.", policy.Name, len(policy.Spec.Ingress))
+				}
+			} else {
+				validateNoIngress(t, policy.Name, policy, expectedPolicy)
+			}
+
+			// 4. Validate Egress Rules
+			hasEgressPolicyType := false
+			for _, pt := range policy.Spec.PolicyTypes {
+				if pt == networkingv1.PolicyTypeEgress {
+					hasEgressPolicyType = true
+					break
+				}
+			}
+
+			if hasEgressPolicyType {
+				switch len(policy.Spec.Egress) {
+				case 0:
+					validateDenyAllEgress(t, policy.Name, expectedPolicy)
+				case 1:
+					validateSingleEgressRule(t, policy.Name, policy.Spec.Egress[0], expectedPolicy)
+				default:
+					assert.Failf(t, "Policy %q in cluster has %d egress rules. Allowed definition supports at most 1 explicit egress rule.", policy.Name, len(policy.Spec.Egress))
+				}
+			} else {
+				validateNoEgress(t, policy, expectedPolicy)
+			}
+		})
+	}
+
+	// 5. Ensure all policies in the registry were found in the cluster
+	assert.Equal(t, len(allowedNetworkPolicies), len(validatedRegistryPolicies),
+		"Mismatch between number of expected policies in registry (%d) and number of policies found & validated in cluster (%d). Missing policies from registry: %v", len(allowedNetworkPolicies), len(validatedRegistryPolicies), missingPolicies(allowedNetworkPolicies, validatedRegistryPolicies))
+}
+
+func missingPolicies(expected map[string]allowedPolicyDefinition, actual map[string]bool) []string {
+	missing := []string{}
+	for k := range expected {
+		if !actual[k] {
+			missing = append(missing, k)
+		}
+	}
+	return missing
+}
+
+// validateNoEgress confirms that a policy which does not have spec.PolicyType=Egress specified
+// has no corresponding egress rules or expectations defined.
+func validateNoEgress(t *testing.T, policy networkingv1.NetworkPolicy, expectedPolicy allowedPolicyDefinition) {
+	// Policy is NOT expected to affect Egress traffic (no Egress in PolicyTypes)
+	// Expected: Cluster has no egress rules; Registry has no DenyAllEgressJustification and empty EgressRule.
+	require.Emptyf(t, policy.Spec.Egress,
+		"Policy %q: Cluster does not have Egress PolicyType, but has Egress rules defined.", policy.Name)
+	require.Emptyf(t, expectedPolicy.denyAllEgressJustification,
+		"Policy %q: Cluster does not have Egress PolicyType. Registry's DenyAllEgressJustification is not empty.", policy.Name)
+	require.Emptyf(t, expectedPolicy.egressRule.ports,
+		"Policy %q: Cluster does not have Egress PolicyType. Registry's EgressRule.Ports is not empty.", policy.Name)
+	require.Emptyf(t, expectedPolicy.egressRule.to,
+		"Policy %q: Cluster does not have Egress PolicyType. Registry's EgressRule.To is not empty.", policy.Name)
+}
+
+// validateDenyAllEgress confirms that a policy with Egress PolicyType but no explicit rules
+// correctly corresponds to a "deny all" expectation.
+func validateDenyAllEgress(t *testing.T, policyName string, expectedPolicy allowedPolicyDefinition) {
+	// Cluster: PolicyType Egress is present, but no explicit egress rules -> Deny All Egress by this policy.
+	// Expected: DenyAllEgressJustification is set; EgressRule.Ports and .To are empty.
+	require.NotEmptyf(t, expectedPolicy.denyAllEgressJustification,
+		"Policy %q: Cluster has Egress PolicyType but no rules (deny all). Registry's DenyAllEgressJustification is empty.", policyName)
+	require.Emptyf(t, expectedPolicy.egressRule.ports,
+		"Policy %q: Cluster has Egress PolicyType but no rules (deny all). Registry's EgressRule.Ports is not empty.", policyName)
+	require.Emptyf(t, expectedPolicy.egressRule.to,
+		"Policy %q: Cluster has Egress PolicyType but no rules (deny all). Registry's EgressRule.To is not empty.", policyName)
+}
+
+// validateSingleEgressRule validates a policy that has exactly one explicit egress rule,
+// distinguishing between "allow-all" and more specific rules.
+func validateSingleEgressRule(t *testing.T, policyName string, clusterEgressRule networkingv1.NetworkPolicyEgressRule, expectedPolicy allowedPolicyDefinition) {
+	// Cluster: PolicyType Egress is present, and there's one explicit egress rule.
+	// Expected: DenyAllEgressJustification is empty; EgressRule matches the cluster's rule.
+	expectedEgressRule := expectedPolicy.egressRule
+
+	require.Emptyf(t, expectedPolicy.denyAllEgressJustification,
+		"Policy %q: Cluster has a specific Egress rule. Registry's DenyAllEgressJustification should be empty.", policyName)
+
+	isClusterRuleAllowAllPorts := len(clusterEgressRule.Ports) == 0
+	isClusterRuleAllowAllPeers := len(clusterEgressRule.To) == 0
+
+	if isClusterRuleAllowAllPorts && isClusterRuleAllowAllPeers { // Handles egress: [{}] - allow all ports to all peers
+		require.Lenf(t, expectedEgressRule.ports, 1,
+			"Policy %q (allow-all egress): Expected EgressRule.Ports to have 1 justification entry, got %d", policyName, len(expectedEgressRule.ports))
+		if len(expectedEgressRule.ports) == 1 { // Guard against panic
+			assert.Nilf(t, expectedEgressRule.ports[0].port,
+				"Policy %q (allow-all egress): Expected EgressRule.Ports[0].Port to be nil, got %+v", policyName, expectedEgressRule.ports[0].port)
+		}
+		assert.Conditionf(t, func() bool { return len(expectedEgressRule.to) == 0 },
+			"Policy %q (allow-all egress): Expected EgressRule.To to be empty for allow-all peers, got %+v", policyName, expectedEgressRule.to)
+	} else {
+		// Specific egress rule (not the simple allow-all ports and allow-all peers)
+		assert.True(t, equality.Semantic.DeepEqual(expectedEgressRule.to, clusterEgressRule.To),
+			"Policy %q, Egress Rule: 'To' mismatch.\nExpected: %+v\nGot:      %+v", policyName, expectedEgressRule.to, clusterEgressRule.To)
+
+		var allExpectedPortsFromPwJ []networkingv1.NetworkPolicyPort
+		for _, pwj := range expectedEgressRule.ports {
+			allExpectedPortsFromPwJ = append(allExpectedPortsFromPwJ, pwj.port...)
+		}
+		require.ElementsMatchf(t, allExpectedPortsFromPwJ, clusterEgressRule.Ports,
+			"Policy %q, Egress Rule: 'Ports' mismatch (aggregated from PortWithJustification). Expected: %+v, Got: %+v", policyName, allExpectedPortsFromPwJ, clusterEgressRule.Ports)
+	}
+}
+
+// validateNoIngress confirms that a policy which does not have the Ingress PolicyType
+// has no corresponding ingress rules or expectations defined.
+func validateNoIngress(t *testing.T, policyName string, clusterPolicy networkingv1.NetworkPolicy, expectedPolicy allowedPolicyDefinition) {
+	// Policy is NOT expected to affect Ingress traffic (no Ingress in PolicyTypes)
+	// Expected: Cluster has no ingress rules; Registry has no DenyAllIngressJustification and empty IngressRule.
+	require.Emptyf(t, clusterPolicy.Spec.Ingress,
+		"Policy %q: Cluster does not have Ingress PolicyType, but has Ingress rules defined.", policyName)
+	require.Emptyf(t, expectedPolicy.denyAllIngressJustification,
+		"Policy %q: Cluster does not have Ingress PolicyType. Registry's DenyAllIngressJustification is not empty.", policyName)
+	require.Emptyf(t, expectedPolicy.ingressRule.ports,
+		"Policy %q: Cluster does not have Ingress PolicyType. Registry's IngressRule.Ports is not empty.", policyName)
+	require.Emptyf(t, expectedPolicy.ingressRule.from,
+		"Policy %q: Cluster does not have Ingress PolicyType. Registry's IngressRule.From is not empty.", policyName)
+}
+
+// validateDenyAllIngress confirms that a policy with Ingress PolicyType but no explicit rules
+// correctly corresponds to a "deny all" expectation.
+func validateDenyAllIngress(t *testing.T, policyName string, expectedPolicy allowedPolicyDefinition) {
+	// Cluster: PolicyType Ingress is present, but no explicit ingress rules -> Deny All Ingress by this policy.
+	// Expected: DenyAllIngressJustification is set; IngressRule.Ports and .From are empty.
+	require.NotEmptyf(t, expectedPolicy.denyAllIngressJustification,
+		"Policy %q: Cluster has Ingress PolicyType but no rules (deny all). Registry's DenyAllIngressJustification is empty.", policyName)
+	require.Emptyf(t, expectedPolicy.ingressRule.ports,
+		"Policy %q: Cluster has Ingress PolicyType but no rules (deny all). Registry's IngressRule.Ports is not empty.", policyName)
+	require.Emptyf(t, expectedPolicy.ingressRule.from,
+		"Policy %q: Cluster has Ingress PolicyType but no rules (deny all). Registry's IngressRule.From is not empty.", policyName)
+}
+
+// validateSingleIngressRule validates a policy that has exactly one explicit ingress rule.
+func validateSingleIngressRule(t *testing.T, policyName string, clusterIngressRule networkingv1.NetworkPolicyIngressRule, expectedPolicy allowedPolicyDefinition) {
+	// Cluster: PolicyType Ingress is present, and there's one explicit ingress rule.
+	// Expected: DenyAllIngressJustification is empty; IngressRule matches the cluster's rule.
+	expectedIngressRule := expectedPolicy.ingressRule
+
+	require.Emptyf(t, expectedPolicy.denyAllIngressJustification,
+		"Policy %q: Cluster has a specific Ingress rule. Registry's DenyAllIngressJustification should be empty.", policyName)
+
+	// Compare 'From'
+	assert.True(t, equality.Semantic.DeepEqual(expectedIngressRule.from, clusterIngressRule.From),
+		"Policy %q, Ingress Rule: 'From' mismatch.\nExpected: %+v\nGot:      %+v", policyName, expectedIngressRule.from, clusterIngressRule.From)
+
+	// Compare 'Ports' by aggregating the ports from our justified structure
+	var allExpectedPortsFromPwJ []networkingv1.NetworkPolicyPort
+	for _, pwj := range expectedIngressRule.ports {
+		allExpectedPortsFromPwJ = append(allExpectedPortsFromPwJ, pwj.port...)
+	}
+	require.ElementsMatchf(t, allExpectedPortsFromPwJ, clusterIngressRule.Ports,
+		"Policy %q, Ingress Rule: 'Ports' mismatch (aggregated from PortWithJustification). Expected: %+v, Got: %+v", policyName, allExpectedPortsFromPwJ, clusterIngressRule.Ports)
+}