Skip to content

Commit

Permalink
Merge pull request #188 from Peac36/update/153
Browse files Browse the repository at this point in the history
Add support for ANP and BANP in the explain command
  • Loading branch information
k8s-ci-robot authored Mar 8, 2024
2 parents ffefa3b + e9a2916 commit 0368c76
Show file tree
Hide file tree
Showing 10 changed files with 1,281 additions and 65 deletions.
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
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:")
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 {
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)
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))
}
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
})

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

0 comments on commit 0368c76

Please sign in to comment.