Skip to content

Commit

Permalink
explainability
Browse files Browse the repository at this point in the history
  • Loading branch information
tanyaveksler committed Dec 10, 2024
1 parent c4811e0 commit 4684411
Show file tree
Hide file tree
Showing 33 changed files with 1,858 additions and 311 deletions.
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
47 changes: 32 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 @@ -201,10 +210,10 @@ func (ca *ConnlistAnalyzer) hasFatalError() error {
func (ca *ConnlistAnalyzer) getPolicyEngine(objectsList []parser.K8sObject) (*eval.PolicyEngine, error) {
// TODO: do we need logger in policyEngine?
if !ca.exposureAnalysis {
return eval.NewPolicyEngineWithObjects(objectsList)
return eval.NewPolicyEngineWithObjects(objectsList, ca.explain)
}
// else build new policy engine with exposure analysis option
pe := eval.NewPolicyEngineWithOptions(ca.exposureAnalysis)
pe := eval.NewPolicyEngineWithOptions(ca.exposureAnalysis, ca.explain)
err := pe.AddObjectsForExposureAnalysis(objectsList)
return pe, err
}
Expand All @@ -225,7 +234,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 @@ -275,7 +284,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 @@ -325,10 +334,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 @@ -344,13 +354,19 @@ func (c *connection) ProtocolsAndPorts() map[v1.Protocol][]common.PortRange {
return c.protocolsAndPorts
}

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

// 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(), "", true)
}
}
connectionSet := &common.ConnectionSet{AllowAll: c.AllProtocolsAndPorts(), AllowedProtocols: protocolsToPortSetMap}
Expand Down Expand Up @@ -538,8 +554,8 @@ func (ca *ConnlistAnalyzer) getConnectionsBetweenPeers(pe *eval.PolicyEngine, pe
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 @@ -636,10 +652,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

0 comments on commit 4684411

Please sign in to comment.