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

dot output with exposure results #333

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
2 changes: 1 addition & 1 deletion pkg/netpol/connlist/connlist.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func (ca *ConnlistAnalyzer) ConnlistFromDirPath(dirPath string) ([]Peer2PeerConn
var ValidFormats = []string{output.TextFormat, output.JSONFormat, output.DOTFormat,
output.CSVFormat, output.MDFormat}

var ExposureValidFormats = []string{output.TextFormat}
var ExposureValidFormats = []string{output.TextFormat, output.DOTFormat}

// ConnlistAnalyzerOption is the type for specifying options for ConnlistAnalyzer,
// using Golang's Options Pattern (https://golang.cafe/blog/golang-functional-options-pattern.html).
Expand Down
58 changes: 36 additions & 22 deletions pkg/netpol/connlist/connlist_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ const DirPathFunc = "ConnlistFromDirPath"
const currentPkg = "connlist"
const notEmptyMsg = "expecting non-empty analysis res"

var allFormats = []string{output.TextFormat, output.JSONFormat, output.CSVFormat, output.MDFormat, output.DOTFormat}
var connlistTestedAPIS = []string{ResourceInfosFunc, DirPathFunc}

/*
Expand Down Expand Up @@ -574,31 +573,31 @@ var goodPathTests = []struct {
},
{
testDirName: "acs_security_frontend_demos",
outputFormats: allFormats,
outputFormats: ValidFormats,
},
{
testDirName: "demo_app_with_routes_and_ingress",
outputFormats: allFormats,
outputFormats: ValidFormats,
},
{
testDirName: "k8s_ingress_test",
outputFormats: allFormats,
outputFormats: ValidFormats,
},
{
testDirName: "multiple_ingress_objects_with_different_ports",
outputFormats: allFormats,
outputFormats: ValidFormats,
},
{
testDirName: "one_ingress_multiple_ports",
outputFormats: allFormats,
outputFormats: ValidFormats,
},
{
testDirName: "one_ingress_multiple_services",
outputFormats: allFormats,
outputFormats: ValidFormats,
},
{
testDirName: "acs-security-demos",
outputFormats: allFormats,
outputFormats: ValidFormats,
},
{
testDirName: "acs-security-demos-with-netpol-list",
Expand All @@ -614,7 +613,7 @@ var goodPathTests = []struct {
},
{
testDirName: "netpol-analysis-example-minimal",
outputFormats: allFormats,
outputFormats: ValidFormats,
},
{
testDirName: "with_end_port_example",
Expand Down Expand Up @@ -753,71 +752,86 @@ var goodPathTests = []struct {
{
testDirName: "acs-security-demos",
exposureAnalysis: true,
outputFormats: []string{output.TextFormat},
outputFormats: ExposureValidFormats,
},
{
testDirName: "test_allow_all",
exposureAnalysis: true,
outputFormats: []string{output.TextFormat},
outputFormats: ExposureValidFormats,
},
{
testDirName: "test_allow_all_in_cluster",
exposureAnalysis: true,
outputFormats: []string{output.TextFormat},
outputFormats: ExposureValidFormats,
},
{
testDirName: "test_allow_egress_deny_ingress",
exposureAnalysis: true,
outputFormats: []string{output.TextFormat},
outputFormats: ExposureValidFormats,
},
{
testDirName: "test_allow_ingress_deny_egress",
exposureAnalysis: true,
outputFormats: []string{output.TextFormat},
outputFormats: ExposureValidFormats,
},
{
testDirName: "test_matched_and_unmatched_rules",
exposureAnalysis: true,
outputFormats: []string{output.TextFormat},
outputFormats: ExposureValidFormats,
},
{
testDirName: "test_only_matched_rules",
exposureAnalysis: true,
outputFormats: []string{output.TextFormat},
outputFormats: ExposureValidFormats,
},
{
testDirName: "test_multiple_unmatched_rules",
exposureAnalysis: true,
outputFormats: []string{output.TextFormat},
outputFormats: ExposureValidFormats,
},
{
testDirName: "test_new_namespace_conn_and_entire_cluster",
exposureAnalysis: true,
outputFormats: []string{output.TextFormat},
outputFormats: ExposureValidFormats,
},
{
testDirName: "test_same_unmatched_rule_in_ingress_egress",
exposureAnalysis: true,
outputFormats: []string{output.TextFormat},
outputFormats: ExposureValidFormats,
},
{
testDirName: "test_with_no_netpols",
exposureAnalysis: true,
outputFormats: []string{output.TextFormat},
outputFormats: ExposureValidFormats,
},
{
testDirName: "test_egress_to_entire_cluster_with_named_ports",
exposureAnalysis: true,
outputFormats: []string{output.TextFormat},
outputFormats: ExposureValidFormats,
},
{
testDirName: "test_ingress_from_entire_cluster_with_named_ports",
exposureAnalysis: true,
outputFormats: []string{output.TextFormat},
outputFormats: ExposureValidFormats,
},
{
testDirName: "test_egress_exposure_with_named_port",
exposureAnalysis: true,
outputFormats: ExposureValidFormats,
},
{
testDirName: "test_exposure_to_namespace_with_multiple_labels",
exposureAnalysis: true,
outputFormats: ExposureValidFormats,
},
{
testDirName: "test_pod_exposed_only_to_representative_peers",
exposureAnalysis: false,
outputFormats: []string{output.TextFormat},
},
{
testDirName: "test_pod_exposed_only_to_representative_peers",
exposureAnalysis: true,
outputFormats: ExposureValidFormats,
},
}
166 changes: 132 additions & 34 deletions pkg/netpol/connlist/conns_formatter_dot.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,29 @@ import (
)

const (
ipColor = "red2"
nonIPPeerColor = "blue"
ipColor = "red2"
nonIPPeerColor = "blue"
representativeObjColor = "red2"
entireClusterShape = " shape=diamond"
peerLineClosing = "]"
allPeersLbl = "all pods"
)

var edgeLineFormat = fmt.Sprintf("\t%%q -> %%q [label=%%q color=\"gold2\" fontcolor=\"darkgreen\"]")
var peerLineFormatPrefix = fmt.Sprintf("\t%%q [label=%%q color=%%q fontcolor=%%q")

// formatDOT: implements the connsFormatter interface for dot output format
type formatDOT struct {
}

// formats an edge line from a singleConnFields struct , to be used for dot graph
// getEdgeLine formats an edge line from a Peer2PeerConnection struct , to be used for dot graph
func getEdgeLine(c Peer2PeerConnection) string {
connStr := common.ConnStrFromConnProperties(c.AllProtocolsAndPorts(), c.ProtocolsAndPorts())
return fmt.Sprintf("\t%q -> %q [label=%q color=\"gold2\" fontcolor=\"darkgreen\"]", c.Src().String(), c.Dst().String(), connStr)
return fmt.Sprintf(edgeLineFormat, c.Src().String(), c.Dst().String(), connStr)
}

// returns the peer label and color to be represented in the graph, and whether the peer is external to cluster's namespaces
// peerNameAndColorByType returns the peer label and color to be represented in the graph, and whether the peer is
// external to cluster's namespaces
func peerNameAndColorByType(peer Peer) (nameLabel, color string, isExternal bool) {
if peer.IsPeerIPType() {
return peer.String(), ipColor, true
Expand All @@ -34,49 +42,139 @@ func peerNameAndColorByType(peer Peer) (nameLabel, color string, isExternal bool
return dotformatting.NodeClusterPeerLabel(peer.Name(), peer.Kind()), nonIPPeerColor, false
}

// formats a peer line for dot graph
// getPeerLine formats a peer line for dot graph
func getPeerLine(peer Peer) (string, bool) {
peerNameLabel, peerColor, isExternalPeer := peerNameAndColorByType(peer)
return fmt.Sprintf("\t%q [label=%q color=%q fontcolor=%q]", peer.String(), peerNameLabel, peerColor, peerColor), isExternalPeer
return fmt.Sprintf(peerLineFormatPrefix+peerLineClosing, peer.String(), peerNameLabel, peerColor, peerColor), isExternalPeer
}

// returns a dot string form of connections from list of Peer2PeerConnection objects
// this format is not supported with exposure analysis; exposureConns is not used;
// and from exposure-analysis results if exists
func (d *formatDOT) writeOutput(conns []Peer2PeerConnection, exposureConns []ExposedPeer) (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
externalPeersLines := make([]string, 0) // list of peers which are not in a cluster's namespace (will not be grouped)
edgeLines := make([]string, len(conns)) // list of edges lines
edgeLines := make([]string, 0) // list of edges lines (connections of connlist + exposure)
peersVisited := make(map[string]bool, 0) // acts as a set
for index := range conns {
srcStr, dstStr := conns[index].Src().String(), conns[index].Dst().String()
edgeLines[index] = getEdgeLine(conns[index])
if !peersVisited[srcStr] {
peersVisited[srcStr] = true
peerLine, isExternalPeer := getPeerLine(conns[index].Src())
if isExternalPeer { // peer that does not belong to a cluster's namespace (i.e. ip/ ingress-controller)
externalPeersLines = append(externalPeersLines, peerLine)
} else { // add to Ns group
dotformatting.AddPeerToNsGroup(conns[index].Src().Namespace(), peerLine, nsPeers)
}
}
if !peersVisited[dstStr] {
peersVisited[dstStr] = true
peerLine, isExternalPeer := getPeerLine(conns[index].Dst())
if isExternalPeer {
externalPeersLines = append(externalPeersLines, peerLine)
} else {
dotformatting.AddPeerToNsGroup(conns[index].Dst().Namespace(), peerLine, nsPeers)
}
}
}
// sort graph lines
// 2. add connlist results to the graph lines
connsEdges, connsExternalPeers := addConnlistOutputData(conns, nsPeers, peersVisited)
edgeLines = append(edgeLines, connsEdges...)
externalPeersLines = append(externalPeersLines, connsExternalPeers...)
// 3. add exposure-analysis results to the graph lines
entireClusterLine, exposureEdges := addExposureOutputData(exposureConns, peersVisited, nsPeers, nsRepPeers)
externalPeersLines = append(externalPeersLines, entireClusterLine...)
edgeLines = append(edgeLines, exposureEdges...)
// 4. sort graph lines
sort.Strings(edgeLines)
sort.Strings(externalPeersLines)
// collect all lines by order
// 5. collect all lines by order
allLines := []string{dotformatting.DotHeader}
allLines = append(allLines, dotformatting.AddNsGroups(nsPeers)...)
allLines = append(allLines, dotformatting.AddNsGroups(nsPeers, dotformatting.DefaultNsGroupColor)...)
allLines = append(allLines, dotformatting.AddNsGroups(nsRepPeers, representativeObjColor)...)
allLines = append(allLines, externalPeersLines...)
allLines = append(allLines, edgeLines...)
allLines = append(allLines, dotformatting.DotClosing)
return strings.Join(allLines, newLineChar), nil
}

// addConnlistOutputData updates namespace peers groups and returns edge lines and external peers lines from connlist results
func addConnlistOutputData(conns []Peer2PeerConnection, nsPeers map[string][]string,
peersVisited map[string]bool) (eLines, externalPeersLines []string) {
edgeLines := make([]string, len(conns))
for index := range conns {
edgeLines[index] = getEdgeLine(conns[index])
externalPeersLines = append(externalPeersLines, addConnlistPeerLine(conns[index].Src(), nsPeers, peersVisited)...)
externalPeersLines = append(externalPeersLines, addConnlistPeerLine(conns[index].Dst(), nsPeers, peersVisited)...)
}
return edgeLines, externalPeersLines
}

// addConnlistPeerLine if the given peer is not visited yet, adds it to the relevant lines' group (namespace group/ external)
func addConnlistPeerLine(peer Peer, nsPeers map[string][]string, peersVisited map[string]bool) (externalPeerLine []string) {
if !peersVisited[peer.String()] {
peersVisited[peer.String()] = true
peerLine, isExternalPeer := getPeerLine(peer)
if isExternalPeer { // peer that does not belong to a cluster's namespace (i.e. ip/ ingress-controller)
externalPeerLine = []string{peerLine}
} else { // add to Ns group
dotformatting.AddPeerToNsGroup(peer.Namespace(), peerLine, nsPeers)
}
}
return externalPeerLine
}

// addExposureOutputData gets the exposure-analysis results, updates the namespaces peers groups lines for both real exposed peers and
// representative peers and returns the exposure edges and entire cluster line (as external peer line)
func addExposureOutputData(exposureConns []ExposedPeer, peersVisited map[string]bool,
nsPeers, nsRepPeers map[string][]string) (entireClusterLine, exposureEdges []string) {
representativeVisited := make(map[string]bool, 0) // acts as a set
for _, ep := range exposureConns {
if !peersVisited[ep.ExposedPeer().String()] { // an exposed peer is a real peer from the manifests,
// updated in the real namespaces map
exposedPeerLine, _ := getPeerLine(ep.ExposedPeer())
dotformatting.AddPeerToNsGroup(ep.ExposedPeer().Namespace(), exposedPeerLine, nsPeers)
}
ingressExpEdges := getXgressExposureEdges(ep.ExposedPeer().String(), ep.IngressExposure(), ep.IsProtectedByIngressNetpols(),
true, representativeVisited, nsRepPeers)
exposureEdges = append(exposureEdges, ingressExpEdges...)
egressExpEdges := getXgressExposureEdges(ep.ExposedPeer().String(), ep.EgressExposure(), ep.IsProtectedByEgressNetpols(),
false, representativeVisited, nsRepPeers)
exposureEdges = append(exposureEdges, egressExpEdges...)
}
// if the entire-cluster marked as visited add its line too (this ensures the entire-cluster is added only once to the graph)
if representativeVisited[entireCluster] {
entireClusterLine = []string{getEntireClusterLine()}
}
return entireClusterLine, exposureEdges
}

// getXgressExposureEdges returns the edges' lines of the exposure data in the given direction ingress/egress
func getXgressExposureEdges(exposedPeerStr string, xgressExpData []XgressExposureData, isProtected, isIngress bool,
representativeVisited map[string]bool, nsRepPeers map[string][]string) (xgressEdges []string) {
if !isProtected { // a connection to entire cluster is enabled, (connection to all ips is already in the graph)
representativeVisited[entireCluster] = true
xgressEdges = append(xgressEdges, getExposureEdgeLine(exposedPeerStr, entireCluster, isIngress, common.MakeConnectionSet(true)))
} else { // protected, having exposure details
for _, data := range xgressExpData {
if data.IsExposedToEntireCluster() {
representativeVisited[entireCluster] = true
xgressEdges = append(xgressEdges, getExposureEdgeLine(exposedPeerStr, entireCluster, isIngress,
data.PotentialConnectivity().(*common.ConnectionSet)))
continue // if a data contains exposure to entire cluster it does not specify labels
}
// @todo consider data.PodLabels
if len(data.NamespaceLabels()) > 0 {
nsRepLabel := convertLabelsMapToString(data.NamespaceLabels())
repPeersStr := allPeersLbl + "_in_" + nsRepLabel // used for getting a unique node name for the peer in the graph
if !representativeVisited[repPeersStr] {
representativeVisited[repPeersStr] = true
dotformatting.AddPeerToNsGroup(peerStrWithNsLabels(data.NamespaceLabels()), getRepPeerLine(repPeersStr), nsRepPeers)
}
xgressEdges = append(xgressEdges, getExposureEdgeLine(exposedPeerStr, repPeersStr, isIngress,
data.PotentialConnectivity().(*common.ConnectionSet)))
}
}
}
return xgressEdges
}

// getEntireClusterLine formats entire cluster line for dot graph
func getEntireClusterLine() string {
return fmt.Sprintf(peerLineFormatPrefix+entireClusterShape+peerLineClosing, entireCluster, entireCluster, representativeObjColor,
representativeObjColor)
}

// getExposureEdgeLine formats an exposure connection edge line for dot graph
func getExposureEdgeLine(realPeerStr, repPeerStr string, isIngress bool, conn *common.ConnectionSet) string {
if isIngress {
return fmt.Sprintf(edgeLineFormat, repPeerStr, realPeerStr, conn.String())
}
return fmt.Sprintf(edgeLineFormat, realPeerStr, repPeerStr, conn.String())
}

// getRepPeerLine formats a representative peer line for dot graph
func getRepPeerLine(peerStr string) string {
// todo : support cases of peer is representative is with pod selector labels
return fmt.Sprintf(peerLineFormatPrefix+peerLineClosing, peerStr, allPeersLbl, representativeObjColor, representativeObjColor)
}
2 changes: 1 addition & 1 deletion pkg/netpol/diff/diff_formatter_dot.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func (df *diffFormatDOT) writeDiffOutput(connsDiff ConnectivityDiff) (string, er

// write graph
allLines := []string{dotformatting.DotHeader}
allLines = append(allLines, dotformatting.AddNsGroups(nsPeers)...)
allLines = append(allLines, dotformatting.AddNsGroups(nsPeers, dotformatting.DefaultNsGroupColor)...)
allLines = append(allLines, externalPeersLines...)
allLines = append(allLines, edgeLines...)
allLines = append(allLines, ingressAnalyzerEdges...)
Expand Down
Loading
Loading