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

explainability #459

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4684411
explainability
tanyaveksler Dec 10, 2024
8217a4b
More delicate handling of intersection of ingress and egress connecti…
tanyaveksler Dec 10, 2024
1e7cbb9
make linter happy
tanyaveksler Dec 23, 2024
faa7ad7
Merge branch 'main' into explainability_new_clean
tanyaveksler Dec 23, 2024
181b9ac
fixing lint errors
tanyaveksler Dec 23, 2024
67ef886
Merge branch 'main' into explainability_new_clean
tanyaveksler Dec 23, 2024
d50485b
More delicate handlinng of IPblock default connections explanation;
tanyaveksler Dec 23, 2024
cd17f64
More compact explainability print
tanyaveksler Jan 6, 2025
326a88e
Fixed lint error
tanyaveksler Jan 6, 2025
4395e77
Merge branch 'main' into explainability_new_clean
tanyaveksler Jan 6, 2025
929c75d
More compact explanation printing.
tanyaveksler Jan 6, 2025
ffe247c
Added more explainability tests.
tanyaveksler Jan 6, 2025
520ef12
Added more explainability tests.
tanyaveksler Jan 7, 2025
9c947f6
Merge branch 'main' into explainability_new_clean
tanyaveksler Jan 7, 2025
409629a
Added a comment
tanyaveksler Jan 7, 2025
48ceb56
Sorting together explanations for specific ports and for all ports in…
tanyaveksler Jan 7, 2025
4686e3e
Added more tests
tanyaveksler Jan 7, 2025
710077a
Added more delicate collect/override implying rules mechanism (consid…
tanyaveksler Jan 13, 2025
49050df
Make linter happy
tanyaveksler Jan 13, 2025
d78e3c4
Changed explanation output for empty Xgress rules;
tanyaveksler Jan 14, 2025
4ab3d96
make linter happy;
tanyaveksler Jan 14, 2025
2f52f7b
make linter happy
tanyaveksler Jan 14, 2025
76c3876
Merge branch 'main' into explainability_new_clean
tanyaveksler Jan 14, 2025
ae74452
changed names of pods in anp_banp_blog_demo test
tanyaveksler Jan 14, 2025
3385cb1
Changed named of pods in anp_banp_blog_demo_2 (according to the chang…
tanyaveksler Jan 14, 2025
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
5 changes: 5 additions & 0 deletions pkg/cli/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
var (
focusWorkload string
exposureAnalysis bool
explain bool
output string // output format
outFile string // output file
)
Expand Down Expand Up @@ -85,6 +86,9 @@ func getConnlistOptions(l *logger.DefaultLogger) []connlist.ConnlistAnalyzerOpti
if exposureAnalysis {
res = append(res, connlist.WithExposureAnalysis())
}
if explain {
res = append(res, connlist.WithExplanation())
}
return res
}

Expand Down Expand Up @@ -130,6 +134,7 @@ defined`,
c.Flags().StringVarP(&focusWorkload, "focusworkload", "", "",
"Focus connections of specified workload in the output (<workload-name> or <workload-namespace/workload-name>)")
c.Flags().BoolVarP(&exposureAnalysis, "exposure", "", false, "Enhance the analysis of permitted connectivity with exposure analysis")
c.Flags().BoolVarP(&explain, "explain", "", false, "Enhance the analysis of permitted connectivity with explainability information")
// output format - default txt
// output format - default txt
supportedFormats := strings.Join(connlist.ValidFormats, ",")
Expand Down
13 changes: 13 additions & 0 deletions pkg/internal/testutils/testutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ var update = flag.Bool("update", false, "write or override golden files")

const (
connlistExpectedOutputFilePartialName = "connlist_output."
explainExpectedOutputFilePartialName = "explain_output."
exposureExpectedOutputFilePartialName = "exposure_output."
underscore = "_"
dotSign = "."
Expand Down Expand Up @@ -56,6 +57,18 @@ func ConnlistTestNameByTestArgs(dirName, focusWorkload, format string, exposureF
return testName, expectedOutputFileName
}

// ExplainTestNameByTestArgs returns explain test name and test's expected output file from some tests args
func ExplainTestNameByTestArgs(dirName, focusWorkload string) (testName, expectedOutputFileName string) {
namePrefix := dirName
if focusWorkload != "" {
namePrefix += focusWlAnnotation + strings.Replace(focusWorkload, "/", underscore, 1)
}
testName = namePrefix
outputPartialName := explainExpectedOutputFilePartialName
expectedOutputFileName = namePrefix + underscore + outputPartialName + output.TextFormat
return testName, expectedOutputFileName
}

// DiffTestNameByTestArgs returns diff test name and test's expected output file from some tests args
func DiffTestNameByTestArgs(ref1, ref2, format string) (testName, expectedOutputFileName string) {
namePrefix := "diff_between_" + ref2 + "_and_" + ref1
Expand Down
50 changes: 35 additions & 15 deletions pkg/netpol/connlist/connlist.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type ConnlistAnalyzer struct {
focusWorkload string
exposureAnalysis bool
exposureResult []ExposedPeer
explain bool
outputFormat string
muteErrsAndWarns bool
peersList []Peer // internally used peersList used in dot formatting; in case of focusWorkload option contains only relevant peers
Expand Down Expand Up @@ -136,6 +137,13 @@ func WithExposureAnalysis() ConnlistAnalyzerOption {
}
}

// WithExplanation is a functional option which directs ConnlistAnalyzer to return explainability of connectivity
func WithExplanation() ConnlistAnalyzerOption {
return func(c *ConnlistAnalyzer) {
c.explain = true
}
}

// WithOutputFormat is a functional option, allowing user to choose the output format txt/json/dot/csv/md.
func WithOutputFormat(outputFormat string) ConnlistAnalyzerOption {
return func(p *ConnlistAnalyzer) {
Expand All @@ -158,6 +166,7 @@ func NewConnlistAnalyzer(options ...ConnlistAnalyzerOption) *ConnlistAnalyzer {
stopOnError: false,
exposureAnalysis: false,
exposureResult: nil,
explain: false,
errors: []ConnlistError{},
outputFormat: output.DefaultFormat,
}
Expand Down Expand Up @@ -200,10 +209,12 @@ func (ca *ConnlistAnalyzer) hasFatalError() error {
// getPolicyEngine returns a new policy engine considering the exposure analysis option
func (ca *ConnlistAnalyzer) getPolicyEngine(objectsList []parser.K8sObject) (*eval.PolicyEngine, error) {
if !ca.exposureAnalysis {
return eval.NewPolicyEngineWithOptionsList(eval.WithLogger(ca.logger), eval.WithObjectsList(objectsList))
return eval.NewPolicyEngineWithOptionsList(eval.WithExplanation(ca.explain),
eval.WithLogger(ca.logger), eval.WithObjectsList(objectsList))
}
// else build new policy engine with exposure analysis option
return eval.NewPolicyEngineWithOptionsList(eval.WithExposureAnalysis(), eval.WithLogger(ca.logger), eval.WithObjectsList(objectsList))
return eval.NewPolicyEngineWithOptionsList(eval.WithExposureAnalysis(), eval.WithExplanation(ca.explain),
eval.WithLogger(ca.logger), eval.WithObjectsList(objectsList))
}

func (ca *ConnlistAnalyzer) connsListFromParsedResources(objectsList []parser.K8sObject) ([]Peer2PeerConnection, []Peer, error) {
Expand All @@ -222,7 +233,7 @@ func (ca *ConnlistAnalyzer) connsListFromParsedResources(objectsList []parser.K8

// ConnlistFromK8sCluster returns the allowed connections list from k8s cluster resources, and list of all peers names
func (ca *ConnlistAnalyzer) ConnlistFromK8sCluster(clientset *kubernetes.Clientset) ([]Peer2PeerConnection, []Peer, error) {
pe := eval.NewPolicyEngineWithOptions(ca.exposureAnalysis)
pe := eval.NewPolicyEngineWithOptions(ca.exposureAnalysis, ca.explain)

// get all resources from k8s cluster
ctx, cancel := context.WithTimeout(context.Background(), ctxTimeoutSeconds*time.Second)
Expand Down Expand Up @@ -272,7 +283,7 @@ func (ca *ConnlistAnalyzer) ConnectionsListToString(conns []Peer2PeerConnection)
ca.errors = append(ca.errors, newResultFormattingError(err))
return "", err
}
out, err := connsFormatter.writeOutput(conns, ca.exposureResult, ca.exposureAnalysis)
out, err := connsFormatter.writeOutput(conns, ca.exposureResult, ca.exposureAnalysis, ca.explain)
if err != nil {
ca.errors = append(ca.errors, newResultFormattingError(err))
return "", err
Expand Down Expand Up @@ -322,10 +333,11 @@ const (

// connection implements the Peer2PeerConnection interface
type connection struct {
src Peer
dst Peer
allConnections bool
protocolsAndPorts map[v1.Protocol][]common.PortRange
src Peer
dst Peer
allConnections bool
commonImplyingRules common.ImplyingRulesType // used for explainability, when allConnections is true
protocolsAndPorts map[v1.Protocol][]common.PortRange
}

func (c *connection) Src() Peer {
Expand All @@ -341,13 +353,20 @@ func (c *connection) ProtocolsAndPorts() map[v1.Protocol][]common.PortRange {
return c.protocolsAndPorts
}

func (c *connection) OnlyDefaultRule() bool {
return c.allConnections && len(c.protocolsAndPorts) == 0 && c.commonImplyingRules.OnlyDefaultRule()
}

// returns a *common.ConnectionSet from Peer2PeerConnection data
func GetConnectionSetFromP2PConnection(c Peer2PeerConnection) *common.ConnectionSet {
protocolsToPortSetMap := make(map[v1.Protocol]*common.PortSet, len(c.ProtocolsAndPorts()))
for protocol, portRangeArr := range c.ProtocolsAndPorts() {
protocolsToPortSetMap[protocol] = common.MakePortSet(false)
for _, p := range portRangeArr {
protocolsToPortSetMap[protocol].AddPortRange(p.Start(), p.End())
augmentedRange := p.(*common.PortRangeData)
// we cannot fill explainability data here, so we pass an empty rule name and an arbitrary direction (isIngress being true)
protocolsToPortSetMap[protocol].AddPortRange(augmentedRange.Start(), augmentedRange.End(),
augmentedRange.InSet(), "", common.DefaultLayer, true)
}
}
connectionSet := &common.ConnectionSet{AllowAll: c.AllProtocolsAndPorts(), AllowedProtocols: protocolsToPortSetMap}
Expand Down Expand Up @@ -578,8 +597,8 @@ func (ca *ConnlistAnalyzer) getConnectionsBetweenPeers(pe *eval.PolicyEngine, sr
return nil, nil, err
}
}
// skip empty connections
if allowedConnections.IsEmpty() {
// skip empty connections when running without explainability
if allowedConnections.IsEmpty() && !ca.explain {
continue
}
p2pConnection, err := ca.getP2PConnOrUpdateExposureConn(pe, allowedConnections, srcPeer, dstPeer, exposureMaps)
Expand Down Expand Up @@ -676,10 +695,11 @@ func (ca *ConnlistAnalyzer) getP2PConnOrUpdateExposureConn(pe *eval.PolicyEngine
// helper function - returns a connection object from the given fields
func createConnectionObject(allowedConnections common.Connection, src, dst Peer) *connection {
return &connection{
src: src,
dst: dst,
allConnections: allowedConnections.IsAllConnections(),
protocolsAndPorts: allowedConnections.ProtocolsAndPortsMap(),
src: src,
dst: dst,
allConnections: allowedConnections.IsAllConnections(),
commonImplyingRules: allowedConnections.(*common.ConnectionSet).CommonImplyingRules,
protocolsAndPorts: allowedConnections.ProtocolsAndPortsMap(true),
}
}

Expand Down
37 changes: 25 additions & 12 deletions pkg/netpol/connlist/conns_formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ type ipMaps struct {

// saveConnsWithIPs gets a P2P connection; if the connection includes an IP-Peer as one of its end-points; the conn is saved in the
// matching map of the formatText maps
func (i *ipMaps) saveConnsWithIPs(conn Peer2PeerConnection) {
func (i *ipMaps) saveConnsWithIPs(conn Peer2PeerConnection, explain bool) {
if conn.Src().IsPeerIPType() {
i.PeerToConnsFromIPs[conn.Dst().String()] = append(i.PeerToConnsFromIPs[conn.Dst().String()], formSingleP2PConn(conn))
i.PeerToConnsFromIPs[conn.Dst().String()] = append(i.PeerToConnsFromIPs[conn.Dst().String()], formSingleP2PConn(conn, explain))
}
if conn.Dst().IsPeerIPType() {
i.peerToConnsToIPs[conn.Src().String()] = append(i.peerToConnsToIPs[conn.Src().String()], formSingleP2PConn(conn))
i.peerToConnsToIPs[conn.Src().String()] = append(i.peerToConnsToIPs[conn.Src().String()], formSingleP2PConn(conn, explain))
}
}

Expand All @@ -57,25 +57,38 @@ func createIPMaps(initMapsFlag bool) (ipMaps ipMaps) {

// connsFormatter implements output formatting in the required output format
type connsFormatter interface {
writeOutput(conns []Peer2PeerConnection, exposureConns []ExposedPeer, exposureFlag bool) (string, error)
writeOutput(conns []Peer2PeerConnection, exposureConns []ExposedPeer, exposureFlag bool, explain bool) (string, error)
}

// singleConnFields represents a single connection object
type singleConnFields struct {
Src string `json:"src"`
Dst string `json:"dst"`
ConnString string `json:"conn"`
Src string `json:"src"`
Dst string `json:"dst"`
ConnString string `json:"conn"`
Explanation string `json:"explanation,omitempty"`
}

// string representation of the singleConnFields struct
func (c singleConnFields) string() string {
return fmt.Sprintf("%s => %s : %s", c.Src, c.Dst, c.ConnString)
}

func (c singleConnFields) nodePairString() string {
return fmt.Sprintf("%s => %s", c.Src, c.Dst)
}

func (c singleConnFields) stringWithExplanation() string {
return fmt.Sprintf("CONNECTIONS BETWEEN %s => %s:\n\n%s", c.Src, c.Dst, c.Explanation)
}

// formSingleP2PConn returns a string representation of single connection fields as singleConnFields object
func formSingleP2PConn(conn Peer2PeerConnection) singleConnFields {
func formSingleP2PConn(conn Peer2PeerConnection, explain bool) singleConnFields {
connStr := common.ConnStrFromConnProperties(conn.AllProtocolsAndPorts(), conn.ProtocolsAndPorts())
return singleConnFields{Src: conn.Src().String(), Dst: conn.Dst().String(), ConnString: connStr}
expl := ""
if explain {
expl = common.ExplanationFromConnProperties(conn.AllProtocolsAndPorts(), conn.(*connection).commonImplyingRules, conn.ProtocolsAndPorts())
}
return singleConnFields{Src: conn.Src().String(), Dst: conn.Dst().String(), ConnString: connStr, Explanation: expl}
}

// commonly (to be) used for exposure analysis output formatters
Expand Down Expand Up @@ -181,13 +194,13 @@ func getRepresentativePodString(podLabels v1.LabelSelector, txtOutFlag bool) str

// getConnlistAsSortedSingleConnFieldsArray returns a sorted singleConnFields list from Peer2PeerConnection list.
// creates ipMaps object if the format requires it (to be used for exposure results later)
func getConnlistAsSortedSingleConnFieldsArray(conns []Peer2PeerConnection, ipMaps ipMaps, saveToIPMaps bool) []singleConnFields {
func getConnlistAsSortedSingleConnFieldsArray(conns []Peer2PeerConnection, ipMaps ipMaps, saveToIPMaps, explain bool) []singleConnFields {
connItems := make([]singleConnFields, len(conns))
for i := range conns {
if saveToIPMaps {
ipMaps.saveConnsWithIPs(conns[i])
ipMaps.saveConnsWithIPs(conns[i], explain)
}
connItems[i] = formSingleP2PConn(conns[i])
connItems[i] = formSingleP2PConn(conns[i], explain)
}
return sortConnFields(connItems, true)
}
Expand Down
9 changes: 5 additions & 4 deletions pkg/netpol/connlist/conns_formatter_csv.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ type formatCSV struct {

// writeOutput returns a CSV string form of connections from list of Peer2PeerConnection objects
// and exposure analysis results from list ExposedPeer if exists
func (cs *formatCSV) writeOutput(conns []Peer2PeerConnection, exposureConns []ExposedPeer, exposureFlag bool) (string, error) {
func (cs *formatCSV) writeOutput(conns []Peer2PeerConnection, exposureConns []ExposedPeer, exposureFlag, explain bool) (string, error) {
// Tanya TODO - handle explain flag
// writing csv rows into a buffer
buf := new(bytes.Buffer)
writer := csv.NewWriter(buf)

err := cs.writeCsvConnlistTable(conns, writer, exposureFlag)
err := cs.writeCsvConnlistTable(conns, writer, exposureFlag, explain)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -61,14 +62,14 @@ func writeTableRows(conns []singleConnFields, writer *csv.Writer, srcFirst bool)
}

// writeCsvConnlistTable writes csv table for the Peer2PeerConnection list
func (cs *formatCSV) writeCsvConnlistTable(conns []Peer2PeerConnection, writer *csv.Writer, saveIPConns bool) error {
func (cs *formatCSV) writeCsvConnlistTable(conns []Peer2PeerConnection, writer *csv.Writer, saveIPConns, explain bool) error {
err := writeCsvColumnsHeader(writer, true)
if err != nil {
return err
}
cs.ipMaps = createIPMaps(saveIPConns)
// get an array of sorted conns items ([]singleConnFields), if required also save the relevant conns to ipMaps
sortedConnItems := getConnlistAsSortedSingleConnFieldsArray(conns, cs.ipMaps, saveIPConns)
sortedConnItems := getConnlistAsSortedSingleConnFieldsArray(conns, cs.ipMaps, saveIPConns, explain)
return writeTableRows(sortedConnItems, writer, true)
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/netpol/connlist/conns_formatter_dot.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func getPeerLine(peer Peer) (string, bool) {

// returns a dot string form of connections from list of Peer2PeerConnection objects
// and from exposure-analysis results if exists
func (d *formatDOT) writeOutput(conns []Peer2PeerConnection, exposureConns []ExposedPeer, exposureFlag bool) (string, error) {
func (d *formatDOT) writeOutput(conns []Peer2PeerConnection, exposureConns []ExposedPeer, exposureFlag, explain bool) (string, error) {
// 1. declaration of maps and slices to be used for forming the graph lines
nsPeers := make(map[string][]string) // map from namespace to its peers (grouping peers by namespaces)
nsRepPeers := make(map[string][]string) // map from representative namespace to its representative peers
Expand Down
5 changes: 3 additions & 2 deletions pkg/netpol/connlist/conns_formatter_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@ type exposureFields struct {

// writeOutput returns a json string form of connections from list of Peer2PeerConnection objects
// and exposure analysis results from list ExposedPeer if exists
func (j *formatJSON) writeOutput(conns []Peer2PeerConnection, exposureConns []ExposedPeer, exposureFlag bool) (string, error) {
func (j *formatJSON) writeOutput(conns []Peer2PeerConnection, exposureConns []ExposedPeer, exposureFlag, explain bool) (string, error) {
// Tanya TODO - handle explain flag
j.ipMaps = createIPMaps(exposureFlag)
// output variables
var jsonConns []byte
var err error
// get an array of sorted connlist items ([]singleConnFields)
sortedConnItems := getConnlistAsSortedSingleConnFieldsArray(conns, j.ipMaps, exposureFlag)
sortedConnItems := getConnlistAsSortedSingleConnFieldsArray(conns, j.ipMaps, exposureFlag, explain)
if exposureFlag {
// get an array of sorted exposure items
ingressExposureItems, egressExposureItems, _ := getExposureConnsAsSortedSingleConnFieldsArray(exposureConns, j.ipMaps)
Expand Down
9 changes: 5 additions & 4 deletions pkg/netpol/connlist/conns_formatter_md.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,10 @@ func getMDLine(c singleConnFields, srcFirst bool) string {

// writeOutput returns a md string form of connections from list of Peer2PeerConnection objects,
// and exposure analysis results from list ExposedPeer if exists
func (md *formatMD) writeOutput(conns []Peer2PeerConnection, exposureConns []ExposedPeer, exposureFlag bool) (string, error) {
func (md *formatMD) writeOutput(conns []Peer2PeerConnection, exposureConns []ExposedPeer, exposureFlag, explain bool) (string, error) {
// Tanya TODO - handle explain flag
// first write connlist lines
allLines := md.writeMdConnlistLines(conns, exposureFlag)
allLines := md.writeMdConnlistLines(conns, exposureFlag, explain)
if !exposureFlag {
return strings.Join(allLines, newLineChar) + newLineChar, nil
}
Expand All @@ -66,9 +67,9 @@ func writeMdLines(conns []singleConnFields, srcFirst bool) []string {
}

// writeMdConnlistLines returns md lines from the list of Peer2PeerConnection
func (md *formatMD) writeMdConnlistLines(conns []Peer2PeerConnection, saveIPConns bool) []string {
func (md *formatMD) writeMdConnlistLines(conns []Peer2PeerConnection, saveIPConns, explain bool) []string {
md.ipMaps = createIPMaps(saveIPConns)
sortedConns := getConnlistAsSortedSingleConnFieldsArray(conns, md.ipMaps, saveIPConns)
sortedConns := getConnlistAsSortedSingleConnFieldsArray(conns, md.ipMaps, saveIPConns, explain)
connlistLines := []string{getMDHeader(true)} // connlist results are formatted: src | dst | conn
connlistLines = append(connlistLines, writeMdLines(sortedConns, true)...)
return connlistLines
Expand Down
Loading
Loading