diff --git a/cmd/list.go b/cmd/list.go
index 03a5861..0610a96 100644
--- a/cmd/list.go
+++ b/cmd/list.go
@@ -5,12 +5,14 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
+	"sort"
 	"strconv"
 	"strings"
 	"text/tabwriter"
 
 	"github.com/cilium/cilium/api/v1/client/endpoint"
 	"github.com/spf13/cobra"
+	"golang.org/x/exp/maps"
 )
 
 func init() {
@@ -40,6 +42,22 @@ type derivedFromEntry struct {
 	Name      string `json:"name"`
 }
 
+func compareDerivedFromEntry(x, y *derivedFromEntry) bool {
+	if x.Direction != y.Direction {
+		return strings.Compare(x.Direction, y.Direction) < 0
+	}
+	if x.Kind != y.Kind {
+		return strings.Compare(x.Kind, y.Kind) < 0
+	}
+	if x.Namespace != y.Namespace {
+		return strings.Compare(x.Namespace, y.Namespace) < 0
+	}
+	if x.Name != y.Name {
+		return strings.Compare(x.Name, y.Name) < 0
+	}
+	return false
+}
+
 func parseDerivedFromEntry(input []string, direction string) derivedFromEntry {
 	val := derivedFromEntry{
 		Direction: direction,
@@ -77,22 +95,27 @@ func runList(ctx context.Context, w io.Writer, name string) error {
 		return err
 	}
 
-	policyList := make([]derivedFromEntry, 0)
+	policySet := make(map[derivedFromEntry]struct{})
 
 	ingressRules := response.Payload.Status.Policy.Realized.L4.Ingress
 	for _, rule := range ingressRules {
 		for _, r := range rule.DerivedFromRules {
-			policyList = append(policyList, parseDerivedFromEntry(r, directionIngress))
+			entry := parseDerivedFromEntry(r, directionIngress)
+			policySet[entry] = struct{}{}
 		}
 	}
 
 	egressRules := response.Payload.Status.Policy.Realized.L4.Egress
 	for _, rule := range egressRules {
 		for _, r := range rule.DerivedFromRules {
-			policyList = append(policyList, parseDerivedFromEntry(r, directionEgress))
+			entry := parseDerivedFromEntry(r, directionEgress)
+			policySet[entry] = struct{}{}
 		}
 	}
 
+	policyList := maps.Keys(policySet)
+	sort.Slice(policyList, func(i, j int) bool { return compareDerivedFromEntry(&policyList[i], &policyList[j]) })
+
 	switch rootOptions.output {
 	case OutputJson:
 		text, err := json.MarshalIndent(policyList, "", "  ")
@@ -103,9 +126,11 @@ func runList(ctx context.Context, w io.Writer, name string) error {
 		return err
 	case OutputSimple:
 		tw := tabwriter.NewWriter(w, 0, 1, 1, ' ', 0)
-		_, err := tw.Write([]byte("DIRECTION\tKIND\tNAMESPACE\tNAME\n"))
-		if err != nil {
-			return err
+		if !rootOptions.noHeaders {
+			_, err := tw.Write([]byte("DIRECTION\tKIND\tNAMESPACE\tNAME\n"))
+			if err != nil {
+				return err
+			}
 		}
 		for _, p := range policyList {
 			_, err := tw.Write([]byte(fmt.Sprintf("%v\t%v\t%v\t%v\n", p.Direction, p.Kind, p.Namespace, p.Name)))
diff --git a/cmd/root.go b/cmd/root.go
index 3451d32..d97d6a8 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -17,6 +17,7 @@ var rootOptions struct {
 	proxySelector string
 	proxyPort     uint16
 	output        string
+	noHeaders     bool
 }
 
 func init() {
@@ -24,6 +25,7 @@ func init() {
 	rootCmd.PersistentFlags().StringVar(&rootOptions.proxySelector, "proxy-selector", "app.kubernetes.io/name=cilium-agent-proxy", "label selector to find the proxy pods")
 	rootCmd.PersistentFlags().Uint16Var(&rootOptions.proxyPort, "proxy-port", 8080, "port number of the proxy endpoints")
 	rootCmd.PersistentFlags().StringVarP(&rootOptions.output, "output", "o", OutputSimple, "output format")
+	rootCmd.PersistentFlags().BoolVar(&rootOptions.noHeaders, "no-headers", false, "stop printing header")
 }
 
 var rootCmd = &cobra.Command{}
diff --git a/e2e/Makefile b/e2e/Makefile
index b2f73e5..d80f725 100644
--- a/e2e/Makefile
+++ b/e2e/Makefile
@@ -33,22 +33,33 @@ start:
 	$(MAKE) --no-print-directory wait-for-workloads
 
 run-test-pod-%:
+	@# https://github.com/orgs/aquaproj/discussions/2964
+	@echo Hello | yq > /dev/null
 	cat testdata/template/ubuntu.yaml | \
 		yq '.metadata.name = "$*"' | \
 		yq '.spec.selector.matchLabels = {"test": "$*"}' | \
-		yq '.spec.template.metadata.labels = {"test": "$*"}' | \
+		yq '.spec.template.metadata.labels = {"test": "$*", "group": "test"}' | \
 		kubectl apply -f -
 
 .PHONY: install-test-pod
 install-test-pod:
 	$(MAKE) --no-print-directory run-test-pod-self
 	$(MAKE) --no-print-directory run-test-pod-l3-ingress-explicit-allow
-	$(MAKE) --no-print-directory run-test-pod-l3-ingress-no-rule
+	$(MAKE) --no-print-directory run-test-pod-l3-ingress-implicit-deny
 	$(MAKE) --no-print-directory run-test-pod-l3-ingress-explicit-deny
 	$(MAKE) --no-print-directory run-test-pod-l3-egress-implicit-deny
 	$(MAKE) --no-print-directory run-test-pod-l3-egress-explicit-deny
+
+	$(MAKE) --no-print-directory run-test-pod-l4-ingress-explicit-allow-any
+	$(MAKE) --no-print-directory run-test-pod-l4-ingress-explicit-allow-tcp
+	$(MAKE) --no-print-directory run-test-pod-l4-ingress-explicit-deny-any
+	$(MAKE) --no-print-directory run-test-pod-l4-ingress-explicit-deny-udp
+	$(MAKE) --no-print-directory run-test-pod-l4-egress-explicit-deny-any
+	$(MAKE) --no-print-directory run-test-pod-l4-egress-explicit-deny-tcp
 	$(MAKE) --no-print-directory wait-for-workloads
+
 	kubectl apply -f testdata/policy/l3.yaml
+	kubectl apply -f testdata/policy/l4.yaml
 
 .PHONY: install-policy-viewer
 install-policy-viewer:
diff --git a/e2e/list_test.go b/e2e/list_test.go
index 2f64dee..949b5f5 100644
--- a/e2e/list_test.go
+++ b/e2e/list_test.go
@@ -1,28 +1,10 @@
 package e2e
 
 import (
-	"encoding/json"
-	"fmt"
-	"reflect"
-
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 )
 
-func testJson(g Gomega, target []byte, expected string) {
-	var t, e interface{}
-	err := json.Unmarshal(target, &t)
-	g.Expect(err).NotTo(HaveOccurred(), "actual: %s", target)
-
-	err = json.Unmarshal([]byte(expected), &e)
-	g.Expect(err).NotTo(HaveOccurred(), "expected: %s", expected)
-
-	if !reflect.DeepEqual(t, e) {
-		err := fmt.Errorf("compare failed. actual: %s, expected: %s", target, expected)
-		g.Expect(err).NotTo(HaveOccurred())
-	}
-}
-
 func testList() {
 	cases := []struct {
 		Selector string
@@ -30,42 +12,80 @@ func testList() {
 	}{
 		{
 			Selector: "test=self",
-			Expected: `[{
-				"direction": "EGRESS",
-				"kind": "CiliumNetworkPolicy",
-				"namespace": "default",
-				"name": "l3-egress"
-			}]`,
+			Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline
+EGRESS,CiliumNetworkPolicy,default,l3-egress
+EGRESS,CiliumNetworkPolicy,default,l4-egress
+INGRESS,CiliumNetworkPolicy,default,l3-baseline`,
 		},
 		{
 			Selector: "test=l3-ingress-explicit-allow",
-			Expected: `[{
-				"direction": "INGRESS",
-				"kind": "CiliumNetworkPolicy",
-				"namespace": "default",
-				"name": "l3-ingress-explicit-allow"
-			}]`,
+			Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline
+INGRESS,CiliumNetworkPolicy,default,l3-baseline
+INGRESS,CiliumNetworkPolicy,default,l3-ingress-explicit-allow`,
 		},
 		{
-			Selector: "test=l3-ingress-no-rule",
-			Expected: `[]`,
+			Selector: "test=l3-ingress-implicit-deny",
+			Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline
+INGRESS,CiliumNetworkPolicy,default,l3-baseline`,
 		},
 		{
 			Selector: "test=l3-ingress-explicit-deny",
-			Expected: `[{
-				"direction": "INGRESS",
-				"kind": "CiliumNetworkPolicy",
-				"namespace": "default",
-				"name": "l3-ingress-explicit-deny"
-			}]`,
+			Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline
+INGRESS,CiliumNetworkPolicy,default,l3-baseline
+INGRESS,CiliumNetworkPolicy,default,l3-ingress-explicit-deny`,
+		},
+		{
+			Selector: "test=l3-egress-implicit-deny",
+			Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline
+INGRESS,CiliumNetworkPolicy,default,l3-baseline`,
+		},
+		{
+			Selector: "test=l3-egress-explicit-deny",
+			Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline
+INGRESS,CiliumNetworkPolicy,default,l3-baseline`,
+		},
+		{
+			Selector: "test=l4-ingress-explicit-allow-any",
+			Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline
+INGRESS,CiliumNetworkPolicy,default,l3-baseline
+INGRESS,CiliumNetworkPolicy,default,l4-ingress-explicit-allow-any`,
+		},
+		{
+			Selector: "test=l4-ingress-explicit-allow-tcp",
+			Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline
+INGRESS,CiliumNetworkPolicy,default,l3-baseline
+INGRESS,CiliumNetworkPolicy,default,l4-ingress-explicit-allow-tcp`,
+		},
+		{
+			Selector: "test=l4-ingress-explicit-deny-any",
+			Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline
+INGRESS,CiliumNetworkPolicy,default,l3-baseline
+INGRESS,CiliumNetworkPolicy,default,l4-ingress-explicit-deny-any`,
+		},
+		{
+			Selector: "test=l4-ingress-explicit-deny-udp",
+			Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline
+INGRESS,CiliumNetworkPolicy,default,l3-baseline
+INGRESS,CiliumNetworkPolicy,default,l4-ingress-explicit-deny-udp`,
+		},
+		{
+			Selector: "test=l4-egress-explicit-deny-any",
+			Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline
+INGRESS,CiliumNetworkPolicy,default,l3-baseline`,
+		},
+		{
+			Selector: "test=l4-egress-explicit-deny-tcp",
+			Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline
+INGRESS,CiliumNetworkPolicy,default,l3-baseline`,
 		},
 	}
 
 	It("should list applied policies", func() {
 		for _, c := range cases {
 			podName := onePodByLabelSelector(Default, "default", c.Selector)
-			result := runViewerSafe(Default, nil, "list", "-o=json", podName)
-			testJson(Default, result, c.Expected)
+			result := runViewerSafe(Default, nil, "list", "-o=json", "--no-headers", podName)
+			result = sedSafe(Default, jqSafe(Default, result, "-r", ".[] | [.direction, .kind, .namespace, .name] | @csv"), `s/\"//g`)
+			Expect(string(result)).To(Equal(c.Expected), "compare failed. selector: %s, actual: %s, expected: %s", c.Selector, string(result), c.Expected)
 		}
 	})
 }
diff --git a/e2e/testdata/policy/README.md b/e2e/testdata/policy/README.md
new file mode 100644
index 0000000..884a997
--- /dev/null
+++ b/e2e/testdata/policy/README.md
@@ -0,0 +1,15 @@
+# NetworkPolicy Configuration for Test Pods
+
+| Target | From self (Egress) | To pod (Ingress) |
+|-|-|-|
+| l3-ingress-explicit-allow | allow | allow |
+| l3-ingress-implicit-deny | allow | - |
+| l3-ingress-explicit-deny | allow | deny |
+| l3-egress-implicit-deny | - | - |
+| l3-egress-explicit-deny | deny | - |
+| l4-ingress-explicit-allow-any | allow | allow (L4) |
+| l4-ingress-explicit-allow-tcp | allow | allow (L4) |
+| l4-ingress-explicit-deny-any | allow | deny (L4) |
+| l4-ingress-explicit-deny-udp | allow | deny (L4) |
+| l4-egress-explicit-deny-any | deny (L4) | - |
+| l4-egress-explicit-deny-tcp | deny (L4) | - |
diff --git a/e2e/testdata/policy/l3.yaml b/e2e/testdata/policy/l3.yaml
index ccc3433..15c42ef 100644
--- a/e2e/testdata/policy/l3.yaml
+++ b/e2e/testdata/policy/l3.yaml
@@ -1,5 +1,22 @@
 apiVersion: cilium.io/v2
 kind: CiliumNetworkPolicy
+metadata:
+  name: l3-baseline
+spec:
+  endpointSelector:
+    matchLabels:
+      k8s:group: test
+  ingressDeny:
+    - fromEndpoints:
+        - matchLabels:
+            k8s:test: scapegoat
+  egressDeny:
+    - toEndpoints:
+        - matchLabels:
+            k8s:test: scapegoat
+---
+apiVersion: cilium.io/v2
+kind: CiliumNetworkPolicy
 metadata:
   name: l3-egress
 spec:
@@ -10,6 +27,9 @@ spec:
     - toEndpoints:
         - matchLabels:
             k8s:test: l3-ingress-explicit-allow
+    - toEndpoints:
+        - matchLabels:
+            k8s:test: l3-ingress-no-rule
     - toEndpoints:
         - matchLabels:
             k8s:test: l3-ingress-implicit-deny
diff --git a/e2e/testdata/policy/l4.yaml b/e2e/testdata/policy/l4.yaml
new file mode 100644
index 0000000..3ef0d76
--- /dev/null
+++ b/e2e/testdata/policy/l4.yaml
@@ -0,0 +1,115 @@
+apiVersion: cilium.io/v2
+kind: CiliumNetworkPolicy
+metadata:
+  name: l4-egress
+spec:
+  endpointSelector:
+    matchLabels:
+      k8s:test: self
+  egress:
+    - toEndpoints:
+        - matchLabels:
+            k8s:test: l4-ingress-explicit-allow-any
+      toPorts:
+        - ports:
+            - port: "53"
+    - toEndpoints:
+        - matchLabels:
+            k8s:test: l4-ingress-explicit-allow-tcp
+      toPorts:
+        - ports:
+            - port: "8080"
+              protocol: TCP
+    - toEndpoints:
+        - matchLabels:
+            k8s:test: l4-ingress-explicit-deny-any
+      toPorts:
+        - ports:
+            - port: "53"
+    - toEndpoints:
+        - matchLabels:
+            k8s:test: l4-ingress-explicit-deny-udp
+      toPorts:
+        - ports:
+            - port: "161" # SNMP (UDP)
+              protocol: UDP
+  egressDeny:
+    - toEndpoints:
+        - matchLabels:
+            k8s:test: l4-egress-explicit-deny-any
+      toPorts:
+        - ports:
+            - port: "53"
+    - toEndpoints:
+        - matchLabels:
+            k8s:test: l4-ingress-explicit-deny-tcp
+      toPorts:
+        - ports:
+            - port: "8080"
+              protocol: TCP
+---
+apiVersion: cilium.io/v2
+kind: CiliumNetworkPolicy
+metadata:
+  name: l4-ingress-explicit-allow-any
+spec:
+  endpointSelector:
+    matchLabels:
+      k8s:test: l4-ingress-explicit-allow-any
+  ingress:
+    - fromEndpoints:
+        - matchLabels:
+            k8s:test: self
+      toPorts:
+        - ports:
+            - port: "53"
+---
+apiVersion: cilium.io/v2
+kind: CiliumNetworkPolicy
+metadata:
+  name: l4-ingress-explicit-allow-tcp
+spec:
+  endpointSelector:
+    matchLabels:
+      k8s:test: l4-ingress-explicit-allow-tcp
+  ingress:
+    - fromEndpoints:
+        - matchLabels:
+            k8s:test: self
+      toPorts:
+        - ports:
+            - port: "8080"
+              protocol: TCP
+---
+apiVersion: cilium.io/v2
+kind: CiliumNetworkPolicy
+metadata:
+  name: l4-ingress-explicit-deny-any
+spec:
+  endpointSelector:
+    matchLabels:
+      k8s:test: l4-ingress-explicit-deny-any
+  ingressDeny:
+    - fromEndpoints:
+        - matchLabels:
+            k8s:test: self
+      toPorts:
+        - ports:
+            - port: "53"
+---
+apiVersion: cilium.io/v2
+kind: CiliumNetworkPolicy
+metadata:
+  name: l4-ingress-explicit-deny-udp
+spec:
+  endpointSelector:
+    matchLabels:
+      k8s:test: l4-ingress-explicit-deny-udp
+  ingressDeny:
+    - fromEndpoints:
+        - matchLabels:
+            k8s:test: self
+      toPorts:
+        - ports:
+            - port: "161" # SNMP (UDP)
+              protocol: UDP
diff --git a/e2e/utils_test.go b/e2e/utils_test.go
index b89dcb9..00e4dcb 100644
--- a/e2e/utils_test.go
+++ b/e2e/utils_test.go
@@ -50,6 +50,12 @@ func jqSafe(g Gomega, input []byte, args ...string) []byte {
 	return []byte(strings.TrimSpace(string(stdout)))
 }
 
+func sedSafe(g Gomega, input []byte, args ...string) []byte {
+	stdout, stderr, err := runCommand("sed", input, args...)
+	g.Expect(err).NotTo(HaveOccurred(), "input: %s, stdout: %s, stderr: %s", string(input), stdout, stderr)
+	return []byte(strings.TrimSpace(string(stdout)))
+}
+
 func runViewer(input []byte, args ...string) ([]byte, []byte, error) {
 	args = append([]string{"exec", "deploy/ubuntu", "--", policyViewerPath}, args...)
 	return kubectl(input, args...)
@@ -65,6 +71,6 @@ func onePodByLabelSelector(g Gomega, namespace, selector string) string {
 	data := kubectlSafe(g, nil, "get", "pod", "-n", namespace, "-l", selector, "-o=json")
 	count, err := strconv.Atoi(string(jqSafe(g, data, "-r", ".items | length")))
 	g.Expect(err).NotTo(HaveOccurred())
-	g.Expect(count).To(Equal(1))
+	g.Expect(count).To(Equal(1), "namespace: %s, selector: %s", namespace, selector)
 	return string(jqSafe(g, data, "-r", ".items[0].metadata.name"))
 }
diff --git a/go.mod b/go.mod
index 704bde4..0904b62 100644
--- a/go.mod
+++ b/go.mod
@@ -7,6 +7,7 @@ require (
 	github.com/onsi/ginkgo/v2 v2.20.2
 	github.com/onsi/gomega v1.34.2
 	github.com/spf13/cobra v1.8.1
+	golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
 	k8s.io/apimachinery v0.29.3
 	k8s.io/client-go v0.29.3
 )
@@ -77,7 +78,6 @@ require (
 	go.opentelemetry.io/otel/trace v1.21.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
-	golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
 	golang.org/x/net v0.28.0 // indirect
 	golang.org/x/oauth2 v0.16.0 // indirect
 	golang.org/x/sync v0.8.0 // indirect