Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for ANP and BANP in the explain command #188

Merged
merged 4 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
869 changes: 869 additions & 0 deletions cmd/policy-assistant/examples/example.go

Large diffs are not rendered by default.

11 changes: 9 additions & 2 deletions cmd/policy-assistant/pkg/cli/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ package cli

import (
"fmt"
"github.com/mattfenwick/cyclonus/examples"
"github.com/mattfenwick/cyclonus/pkg/kube/netpol"
"sigs.k8s.io/network-policy-api/apis/v1alpha1"
"strings"

"github.com/mattfenwick/collections/pkg/json"
"github.com/mattfenwick/cyclonus/pkg/connectivity/probe"
"github.com/mattfenwick/cyclonus/pkg/generator"

"github.com/mattfenwick/cyclonus/pkg/kube"
"github.com/mattfenwick/cyclonus/pkg/kube/netpol"
"github.com/mattfenwick/cyclonus/pkg/matcher"
"github.com/mattfenwick/cyclonus/pkg/utils"
"github.com/pkg/errors"
Expand Down Expand Up @@ -87,6 +89,8 @@ func SetupAnalyzeCommand() *cobra.Command {
func RunAnalyzeCommand(args *AnalyzeArgs) {
// 1. read policies from kube
var kubePolicies []*networkingv1.NetworkPolicy
var kubeANPs []*v1alpha1.AdminNetworkPolicy
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for a follow-up PR: set these via CLI (#201)

var kubeBANPs *v1alpha1.BaselineAdminNetworkPolicy
var kubePods []v1.Pod
var kubeNamespaces []v1.Namespace
if args.AllNamespaces || len(args.Namespaces) > 0 {
Expand Down Expand Up @@ -118,10 +122,13 @@ func RunAnalyzeCommand(args *AnalyzeArgs) {
// 3. read example policies
if args.UseExamplePolicies {
kubePolicies = append(kubePolicies, netpol.AllExamples...)

kubeANPs = examples.CoreGressRulesCombinedANB
kubeBANPs = examples.CoreGressRulesCombinedBANB
}

logrus.Debugf("parsed policies:\n%s", json.MustMarshalToString(kubePolicies))
policies := matcher.BuildNetworkPolicies(args.SimplifyPolicies, kubePolicies)
policies := matcher.BuildV1AndV2NetPols(args.SimplifyPolicies, kubePolicies, kubeANPs, kubeBANPs)

for _, mode := range args.Modes {
switch mode {
Expand Down
6 changes: 2 additions & 4 deletions cmd/policy-assistant/pkg/kube/labelselector.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,19 +107,17 @@ func LabelSelectorTableLines(selector metav1.LabelSelector) string {
}
var lines []string
if len(selector.MatchLabels) > 0 {
lines = append(lines, "Match labels:")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we keep this and Match expressions: ? Similarly to my previous comment, leaving this will make the output clearer imo

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or feel free to change the wording, and we can assess outputs for both the MatchLabels case and MatchExpressions

Copy link
Contributor Author

@Peac36 Peac36 Feb 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep it that way. We can change it later when we are sure for the output layout.

for _, key := range slice.Sort(maps.Keys(selector.MatchLabels)) {
val := selector.MatchLabels[key]
lines = append(lines, fmt.Sprintf(" %s: %s", key, val))
lines = append(lines, fmt.Sprintf(" %s = %s", key, val))
}
}
if len(selector.MatchExpressions) > 0 {
lines = append(lines, "Match expressions:")
sortedMatchExpressions := slice.SortOn(
func(l metav1.LabelSelectorRequirement) string { return l.Key },
selector.MatchExpressions)
for _, exp := range sortedMatchExpressions {
lines = append(lines, fmt.Sprintf(" %s %s %+v", exp.Key, exp.Operator, slice.Sort(exp.Values)))
lines = append(lines, fmt.Sprintf(" %s %s %+v", exp.Key, exp.Operator, slice.Sort(exp.Values)))
}
}
return strings.Join(lines, "\n")
Expand Down
8 changes: 4 additions & 4 deletions cmd/policy-assistant/pkg/matcher/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ func BuildTargetANP(anp *v1alpha1.AdminNetworkPolicy) (*Target, *Target) {
v := AdminActionToVerdict(r.Action)
matchers := BuildPeerMatcherAdmin(r.From, r.Ports)
for _, m := range matchers {
matcherAdmin := NewPeerMatcherANP(m, v, int(anp.Spec.Priority))
matcherAdmin := NewPeerMatcherANP(m, v, int(anp.Spec.Priority), anp.Name)
ingress.Peers = append(ingress.Peers, matcherAdmin)
}
}
Expand All @@ -240,7 +240,7 @@ func BuildTargetANP(anp *v1alpha1.AdminNetworkPolicy) (*Target, *Target) {
v := AdminActionToVerdict(r.Action)
matchers := BuildPeerMatcherAdmin(r.To, r.Ports)
for _, m := range matchers {
matcherAdmin := NewPeerMatcherANP(m, v, int(anp.Spec.Priority))
matcherAdmin := NewPeerMatcherANP(m, v, int(anp.Spec.Priority), anp.Name)
egress.Peers = append(egress.Peers, matcherAdmin)
}
}
Expand All @@ -267,7 +267,7 @@ func BuildTargetBANP(banp *v1alpha1.BaselineAdminNetworkPolicy) (*Target, *Targe
v := BaselineAdminActionToVerdict(r.Action)
matchers := BuildPeerMatcherAdmin(r.From, r.Ports)
for _, m := range matchers {
matcherAdmin := NewPeerMatcherBANP(m, v)
matcherAdmin := NewPeerMatcherBANP(m, v, banp.Name)
ingress.Peers = append(ingress.Peers, matcherAdmin)
}
}
Expand All @@ -283,7 +283,7 @@ func BuildTargetBANP(banp *v1alpha1.BaselineAdminNetworkPolicy) (*Target, *Targe
v := BaselineAdminActionToVerdict(r.Action)
matchers := BuildPeerMatcherAdmin(r.To, r.Ports)
for _, m := range matchers {
matcherAdmin := NewPeerMatcherBANP(m, v)
matcherAdmin := NewPeerMatcherBANP(m, v, banp.Name)
egress.Peers = append(egress.Peers, matcherAdmin)
}
}
Expand Down
12 changes: 12 additions & 0 deletions cmd/policy-assistant/pkg/matcher/builder_tests.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package matcher

import (
"github.com/mattfenwick/cyclonus/examples"
"github.com/mattfenwick/cyclonus/pkg/kube/netpol"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"golang.org/x/exp/maps"
v1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -339,4 +341,14 @@ func RunBuilderTests() {
}}}))
})
})

Describe("BuildV1AndV2NetPols", func() {
It("it combines ANPs with same subject", func() {
result := BuildV1AndV2NetPols(true, nil, examples.SimpleANPs, nil)
Expect(result.Egress).To(HaveLen(1))
k := maps.Keys(result.Egress)
firstRule := result.Egress[k[0]]
Expect(firstRule.SourceRules).To(HaveLen(2))
})
})
}
216 changes: 168 additions & 48 deletions cmd/policy-assistant/pkg/matcher/explain.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,36 @@ package matcher

import (
"fmt"
"strings"

"github.com/mattfenwick/collections/pkg/json"
"github.com/mattfenwick/collections/pkg/slice"
"golang.org/x/exp/slices"
v1 "k8s.io/api/core/v1"
"strings"

"github.com/mattfenwick/cyclonus/pkg/kube"
"github.com/olekukonko/tablewriter"
"github.com/pkg/errors"
)

// peerProtocolGroup groups all anps and banps in single struct
type peerProtocolGroup struct {
port string
subject string
policies map[string]*anpGroup
}

// dummy implementation of the interface so we add the struct to the targer peers
func (p *peerProtocolGroup) Matches(subject, peer *TrafficPeer, portInt int, portName string, protocol v1.Protocol) bool {
Peac36 marked this conversation as resolved.
Show resolved Hide resolved
return false
}

type anpGroup struct {
name string
priority int
effects []string
kind PolicyKind
}

type SliceBuilder struct {
Prefix []string
Elements [][]string
Expand All @@ -26,14 +47,14 @@ func (p *Policy) ExplainTable() string {
table.SetAutoWrapText(false)
table.SetRowLine(true)
table.SetAutoMergeCells(true)
// FIXME add action/priority column
table.SetHeader([]string{"Type", "Subject", "Source rules", "Peer", "Port/Protocol"})

table.SetHeader([]string{"Type", "Subject", "Source rules", "Peer", "Action", "Port/Protocol"})

builder := &SliceBuilder{}
ingresses, egresses := p.SortedTargets()
builder.TargetsTableLines(ingresses, true)
// FIXME add action/priority column
builder.Elements = append(builder.Elements, []string{"", "", "", "", ""})

builder.Elements = append(builder.Elements, []string{"", "", "", "", "", ""})
builder.TargetsTableLines(egresses, false)

table.AppendBulk(builder.Elements)
Expand All @@ -50,68 +71,89 @@ func (s *SliceBuilder) TargetsTableLines(targets []*Target, isIngress bool) {
ruleType = "Egress"
}
for _, target := range targets {
sourceRules := slice.Sort(target.SourceRules)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend sorting all output so that it's:

  • deterministic
  • predictable for the reader

sourceRules := target.SourceRules
sourceRulesStrings := make([]string, 0, len(sourceRules))
for _, rule := range sourceRules {
sourceRulesStrings = append(sourceRulesStrings, string(rule))
}
slices.Sort(sourceRulesStrings)
rules := strings.Join(sourceRulesStrings, "\n")
s.Prefix = []string{ruleType, target.TargetString(), rules}

if len(target.Peers) == 0 {
s.Append("no pods, no ips", "no ports, no protocols")
} else {
for _, peer := range slice.SortOn(func(p PeerMatcher) string { return json.MustMarshalToString(p) }, target.Peers) {
switch a := peer.(type) {
case *PeerMatcherAdmin:
s.PodPeerMatcherTableLines(a.PodPeerMatcher, a.effectFromMatch)
case *AllPeersMatcher:
s.Append("all pods, all ips", "all ports, all protocols")
case *PortsForAllPeersMatcher:
pps := PortMatcherTableLines(a.Port, NetworkPolicyV1)
s.Append("all pods, all ips", strings.Join(pps, "\n"))
case *IPPeerMatcher:
s.IPPeerMatcherTableLines(a)
case *PodPeerMatcher:
s.PodPeerMatcherTableLines(a, NewV1Effect(true))
default:
panic(errors.Errorf("invalid PeerMatcher type %T", a))
Peac36 marked this conversation as resolved.
Show resolved Hide resolved
}
s.Append("no pods, no ips", "NPv1: All peers allowed", "no ports, no protocols")
continue
}

peers := groupAnbAndBanp(target.Peers)
for _, p := range slice.SortOn(func(p PeerMatcher) string { return json.MustMarshalToString(p) }, peers) {
switch t := p.(type) {
case *AllPeersMatcher:
s.Append("all pods, all ips", "NPv1: All peers allowed", "all ports, all protocols")
case *PortsForAllPeersMatcher:
pps := PortMatcherTableLines(t.Port, NetworkPolicyV1)
s.Append("all pods, all ips", "NPv1: All peers allowed", strings.Join(pps, "\n"))
case *IPPeerMatcher:
s.IPPeerMatcherTableLines(t)
case *PodPeerMatcher:
s.Append(resolveSubject(t), "NPv1: All peers allowed", strings.Join(PortMatcherTableLines(t.Port, NewV1Effect(true).PolicyKind), "\n"))
case *peerProtocolGroup:
s.peerProtocolGroupTableLines(t)
default:
panic(errors.Errorf("invalid PeerMatcher type %T", p))
}
}

}
}

func (s *SliceBuilder) IPPeerMatcherTableLines(ip *IPPeerMatcher) {
peer := ip.IPBlock.CIDR + "\n" + fmt.Sprintf("except %+v", ip.IPBlock.Except)
pps := PortMatcherTableLines(ip.Port, NetworkPolicyV1)
s.Append(peer, strings.Join(pps, "\n"))
s.Append(peer, "NPv1: All peers allowed", strings.Join(pps, "\n"))
}

func (s *SliceBuilder) PodPeerMatcherTableLines(nsPodMatcher *PodPeerMatcher, e Effect) {
// FIXME add action/priority column using fields of the Effect parameter "e"
var namespaces string
switch ns := nsPodMatcher.Namespace.(type) {
case *AllNamespaceMatcher:
namespaces = "all"
case *LabelSelectorNamespaceMatcher:
namespaces = kube.LabelSelectorTableLines(ns.Selector)
// FIXME handle SameLabels, NotSameLabels
case *ExactNamespaceMatcher:
namespaces = ns.Namespace
default:
panic(errors.Errorf("invalid NamespaceMatcher type %T", ns))
func (s *SliceBuilder) peerProtocolGroupTableLines(t *peerProtocolGroup) {
actions := []string{}

anps := make([]*anpGroup, 0, len(t.policies))
for _, v := range t.policies {
if v.kind == AdminNetworkPolicy {
anps = append(anps, v)
}
}
var pods string
switch p := nsPodMatcher.Pod.(type) {
case *AllPodMatcher:
pods = "all"
case *LabelSelectorPodMatcher:
pods = kube.LabelSelectorTableLines(p.Selector)
default:
panic(errors.Errorf("invalid PodMatcher type %T", p))
if len(anps) > 0 {
actions = append(actions, "ANP:")
slices.SortFunc(anps, func(a, b *anpGroup) bool {
return a.priority < b.priority
})
for _, v := range anps {
if len(v.effects) > 1 {
actions = append(actions, fmt.Sprintf(" pri=%d (%s): %s (ineffective rules: %s)", v.priority, v.name, v.effects[0], strings.Join(v.effects[1:], ", ")))
} else {
actions = append(actions, fmt.Sprintf(" pri=%d (%s): %s", v.priority, v.name, v.effects[0]))
}
}
}

banps := make([]*anpGroup, 0, len(t.policies))
for _, v := range t.policies {
if v.kind == BaselineAdminNetworkPolicy {
banps = append(banps, v)
}
}
if len(banps) > 0 {
actions = append(actions, "BANP:")
for _, v := range banps {
if len(v.effects) > 1 {
actions = append(actions, fmt.Sprintf(" %s (ineffective rules: %s)", v.effects[0], strings.Join(v.effects[1:], ", ")))
} else {
actions = append(actions, fmt.Sprintf(" %s", v.effects[0]))
}
}
}
s.Append("namespace: "+namespaces+"\n"+"pods: "+pods, strings.Join(PortMatcherTableLines(nsPodMatcher.Port, e.PolicyKind), "\n"))

s.Append(t.subject, strings.Join(actions, "\n"), t.port)
}

func PortMatcherTableLines(pm PortMatcher, kind PolicyKind) []string {
Expand Down Expand Up @@ -141,3 +183,81 @@ func PortMatcherTableLines(pm PortMatcher, kind PolicyKind) []string {
panic(errors.Errorf("invalid PortMatcher type %T", port))
}
}

func groupAnbAndBanp(p []PeerMatcher) []PeerMatcher {
result := make([]PeerMatcher, 0, len(p))
groups := map[string]*peerProtocolGroup{}

for _, v := range p {
switch t := v.(type) {
case *PeerMatcherAdmin:
k := t.Port.GetPrimaryKey() + t.Pod.PrimaryKey() + t.Namespace.PrimaryKey()
if _, ok := groups[k]; !ok {
groups[k] = &peerProtocolGroup{
port: strings.Join(PortMatcherTableLines(t.PodPeerMatcher.Port, t.effectFromMatch.PolicyKind), "\n"),
subject: resolveSubject(t.PodPeerMatcher),
policies: map[string]*anpGroup{},
}
}
kg := t.Name
if _, ok := groups[k].policies[kg]; !ok {
groups[k].policies[kg] = &anpGroup{
name: t.Name,
priority: t.effectFromMatch.Priority,
effects: []string{},
kind: t.effectFromMatch.PolicyKind,
}
}
groups[k].policies[kg].effects = append(groups[k].policies[kg].effects, string(t.effectFromMatch.Verdict))
default:
result = append(result, v)
}
}

groupResult := make([]*peerProtocolGroup, 0, len(groups))
for _, v := range groups {
groupResult = append(groupResult, v)
}
slices.SortFunc(groupResult, func(a, b *peerProtocolGroup) bool {
if a.port == b.port {
return a.subject < b.subject
}
return a.port < b.port
Peac36 marked this conversation as resolved.
Show resolved Hide resolved
})

for _, v := range groupResult {
result = append(result, v)
}

return result
}

func resolveSubject(nsPodMatcher *PodPeerMatcher) string {
var namespaces string
var pods string
switch ns := nsPodMatcher.Namespace.(type) {
case *AllNamespaceMatcher:
namespaces = "all"
case *LabelSelectorNamespaceMatcher:
namespaces = kube.LabelSelectorTableLines(ns.Selector)
case *SameLabelsNamespaceMatcher:
namespaces = fmt.Sprintf("Same labels - %s", strings.Join(ns.labels, ", "))
case *NotSameLabelsNamespaceMatcher:
namespaces = fmt.Sprintf("Not Same labels - %s", strings.Join(ns.labels, ", "))
case *ExactNamespaceMatcher:
namespaces = ns.Namespace
default:
panic(errors.Errorf("invalid NamespaceMatcher type %T", ns))
}

switch p := nsPodMatcher.Pod.(type) {
case *AllPodMatcher:
pods = "all"
case *LabelSelectorPodMatcher:
pods = kube.LabelSelectorTableLines(p.Selector)
default:
panic(errors.Errorf("invalid PodMatcher type %T", p))
}

return fmt.Sprintf("Namespace:\n %s\nPod:\n %s", strings.TrimSpace(namespaces), strings.TrimSpace(pods))
}
Loading