diff --git a/pkg/ibmvpc/explainability_test.go b/pkg/ibmvpc/explainability_test.go index a62880e36..8a1e99192 100644 --- a/pkg/ibmvpc/explainability_test.go +++ b/pkg/ibmvpc/explainability_test.go @@ -13,7 +13,7 @@ import ( ) // todo: quick and dirty tmp until added to the cli, by which these will be added as end-to-end tests -func TestExplainability1(t *testing.T) { +func TestVsiToVsi(t *testing.T) { vpcConfig := getConfig(t, "input_sg_testing1_new.json") if vpcConfig == nil { require.Fail(t, "vpcConfig equals nil") @@ -28,8 +28,9 @@ func TestExplainability1(t *testing.T) { "SecurityGroupLayer Rules\n------------------------\nenabling rules from sg2-ky:"+ "\n\tindex: 5, direction: outbound, protocol: all, cidr: 10.240.30.0/24"+ "\n\tindex: 6, direction: outbound, conns: protocol: tcp, dstPorts: 1-65535, cidr: 10.240.20.4/32,10.240.30.4/32"+ - "\n\nIngress Rules:\n~~~~~~~~~~~~~~\nSecurityGroupLayer Rules\n------------------------\nenabling rules from sg2-ky:"+ - "\n\tindex: 7, direction: inbound, conns: protocol: tcp, dstPorts: 1-65535, cidr: 10.240.20.4/32,10.240.30.4/32\n\n", explanbilityStr1) + "\nIngress Rules:\n~~~~~~~~~~~~~~\nSecurityGroupLayer Rules\n------------------------\nenabling rules from sg2-ky:"+ + "\n\tindex: 7, direction: inbound, conns: protocol: tcp, dstPorts: 1-65535, cidr: 10.240.20.4/32,10.240.30.4/32\n\n", + explanbilityStr1) explanbilityStr2, err2 := vpcConfig.ExplainConnectivity("vsi2-ky[10.240.20.4]", "vsi1-ky[10.240.10.4]") if err2 != nil { require.Fail(t, err2.Error()) @@ -38,7 +39,7 @@ func TestExplainability1(t *testing.T) { require.Equal(t, "The following connection exists between vsi2-ky[10.240.20.4] and vsi1-ky[10.240.10.4]: "+ "All Connections; its enabled by\nEgress Rules:\n~~~~~~~~~~~~~\n"+ "SecurityGroupLayer Rules\n------------------------\nenabling rules from sg2-ky:"+ - "\n\tindex: 1, direction: outbound, protocol: all, cidr: 10.240.10.0/24\n\nIngress Rules:"+ + "\n\tindex: 1, direction: outbound, protocol: all, cidr: 10.240.10.0/24\nIngress Rules:"+ "\n~~~~~~~~~~~~~~\nSecurityGroupLayer Rules\n------------------------\nenabling rules from sg1-ky:\n\t"+ "index: 3, direction: inbound, protocol: all, cidr: 10.240.20.4/32,10.240.30.4/32\n\n", explanbilityStr2) explanbilityStr3, err3 := vpcConfig.ExplainConnectivity("vsi3a-ky[10.240.30.5]", "vsi1-ky[10.240.10.4]") @@ -49,7 +50,7 @@ func TestExplainability1(t *testing.T) { require.Equal(t, "The following connection exists between vsi3a-ky[10.240.30.5] and vsi1-ky[10.240.10.4]: "+ "All Connections; its enabled by\n"+ "Egress Rules:\n~~~~~~~~~~~~~\nSecurityGroupLayer Rules\n------------------------\nenabling rules from sg3-ky:\n"+ - "\tindex: 0, direction: outbound, protocol: all, cidr: 0.0.0.0/0\n\nIngress Rules:\n~~~~~~~~~~~~~~\nSecurityGroupLayer Rules"+ + "\tindex: 0, direction: outbound, protocol: all, cidr: 0.0.0.0/0\nIngress Rules:\n~~~~~~~~~~~~~~\nSecurityGroupLayer Rules"+ "\n------------------------\nenabling rules from sg1-ky:\n"+ "\tindex: 4, direction: inbound, protocol: all, cidr: 10.240.30.5/32,10.240.30.6/32\n\n", explanbilityStr3) explanbilityStr4, err4 := vpcConfig.ExplainConnectivity("vsi1-ky[10.240.10.4]", "vsi2-ky[10.240.20.4]") @@ -58,16 +59,16 @@ func TestExplainability1(t *testing.T) { } fmt.Println(explanbilityStr4) require.Equal(t, "No connection between vsi1-ky[10.240.10.4] and vsi2-ky[10.240.20.4]; "+ - "connection blocked by egress\nIngress Rules:\n~~~~~~~~~~~~~\nSecurityGroupLayer Rules\n------------------------\n"+ - "enabling rules from sg2-ky:\n\tindex: 4, direction: inbound, protocol: all, cidr: 10.240.10.4/32\n", explanbilityStr4) + "connection blocked by egress\nIngress Rules:\n~~~~~~~~~~~~~~\nSecurityGroupLayer Rules\n------------------------\n"+ + "enabling rules from sg2-ky:\n\tindex: 4, direction: inbound, protocol: all, cidr: 10.240.10.4/32\n\n", explanbilityStr4) explanbilityStr5, err5 := vpcConfig.ExplainConnectivity("vsi3a-ky[10.240.30.5]", "vsi2-ky[10.240.20.4]") if err5 != nil { require.Fail(t, err5.Error()) } fmt.Println(explanbilityStr5) require.Equal(t, "No connection between vsi3a-ky[10.240.30.5] and vsi2-ky[10.240.20.4]; connection blocked by ingress\n"+ - "Egress Rules:\n~~~~~~~~~~~~~~\nSecurityGroupLayer Rules\n------------------------\nenabling rules from sg3-ky:"+ - "\n\tindex: 0, direction: outbound, protocol: all, cidr: 0.0.0.0/0\n", explanbilityStr5) + "Egress Rules:\n~~~~~~~~~~~~~\nSecurityGroupLayer Rules\n------------------------\nenabling rules from sg3-ky:"+ + "\n\tindex: 0, direction: outbound, protocol: all, cidr: 0.0.0.0/0\n\n", explanbilityStr5) fmt.Println("done") } @@ -76,7 +77,7 @@ func TestExplainability1(t *testing.T) { // sg3-ky: vsi3a-ky // sg1-ky, sg3-ky: default // sg2-ky: allow all -func TestExplainability2(t *testing.T) { +func TestSGDefaultRules(t *testing.T) { vpcConfig := getConfig(t, "input_sg_testing_default.json") if vpcConfig == nil { require.Fail(t, "vpcConfig equals nil") @@ -88,9 +89,9 @@ func TestExplainability2(t *testing.T) { } fmt.Println(explanbilityStr1) require.Equal(t, "No connection between vsi1-ky[10.240.10.4] and vsi3a-ky[10.240.30.5]; "+ - "connection blocked by ingress\nEgress Rules:\n~~~~~~~~~~~~~~\nSecurityGroupLayer Rules\n"+ + "connection blocked by ingress\nEgress Rules:\n~~~~~~~~~~~~~\nSecurityGroupLayer Rules\n"+ "------------------------\nrules in sg1-ky are the default, namely this is the enabling egress rule:\n"+ - "\tindex: 0, direction: outbound, protocol: all, cidr: 0.0.0.0/0\n", explanbilityStr1) + "\tindex: 0, direction: outbound, protocol: all, cidr: 0.0.0.0/0\n\n", explanbilityStr1) // connection, egress (sg3-ky) is default explanbilityStr2, err2 := vpcConfig.ExplainConnectivity("vsi3a-ky[10.240.30.5]", "vsi2-ky[10.240.20.4]") if err2 != nil { @@ -100,12 +101,117 @@ func TestExplainability2(t *testing.T) { require.Equal(t, "The following connection exists between vsi3a-ky[10.240.30.5] and vsi2-ky[10.240.20.4]: All Connections; "+ "its enabled by\nEgress Rules:\n~~~~~~~~~~~~~\nSecurityGroupLayer Rules\n------------------------\n"+ "rules in sg3-ky are the default, namely this is the enabling egress rule:\n"+ - "\tindex: 0, direction: outbound, protocol: all, cidr: 0.0.0.0/0\n\n"+ + "\tindex: 0, direction: outbound, protocol: all, cidr: 0.0.0.0/0\n"+ "Ingress Rules:\n~~~~~~~~~~~~~~\nSecurityGroupLayer Rules\n------------------------\n"+ "enabling rules from sg2-ky:\n\tindex: 1, direction: inbound, protocol: all, cidr: 0.0.0.0/0\n\n", explanbilityStr2) fmt.Println("done") } +func TestInputValidity(t *testing.T) { + vpcConfig := getConfig(t, "input_sg_testing1_new.json") + if vpcConfig == nil { + require.Fail(t, "vpcConfig equals nil") + } + cidr1 := "0.0.0.0/0" + cidr2 := "161.26.0.0/16" + nonExistingVSI := "vsi2-ky[10.240.10.4]" + _, err1 := vpcConfig.ExplainConnectivity(cidr1, cidr2) + fmt.Println(err1.Error()) + if err1 == nil { + require.Fail(t, err1.Error()) + } + _, err2 := vpcConfig.ExplainConnectivity(cidr1, nonExistingVSI) + fmt.Println(err2.Error()) + if err2 == nil { + require.Fail(t, err1.Error()) + } +} + +func TestSimpleExternal(t *testing.T) { + vpcConfig := getConfig(t, "input_sg_testing1_new.json") + if vpcConfig == nil { + require.Fail(t, "vpcConfig equals nil") + } + vsi1 := "vsi1-ky[10.240.10.4]" + cidr1 := "161.26.0.0/16" + cidr2 := "161.26.0.0/32" + explanbilityStr1, err1 := vpcConfig.ExplainConnectivity(vsi1, cidr1) + if err1 != nil { + require.Fail(t, err1.Error()) + } + require.Equal(t, "The following connection exists between vsi1-ky[10.240.10.4] and Public Internet 161.26.0.0/16: "+ + "protocol: UDP; its enabled by\n"+ + "Egress Rules:\n~~~~~~~~~~~~~\nSecurityGroupLayer Rules\n------------------------\nenabling rules from sg1-ky:\n\t"+ + "index: 2, direction: outbound, conns: protocol: udp, dstPorts: 1-65535, cidr: 161.26.0.0/16\n\n", explanbilityStr1) + fmt.Println(explanbilityStr1) + fmt.Println("---------------------------------------------------------------------------------------------------------------------------") + explanbilityStr2, err2 := vpcConfig.ExplainConnectivity(cidr1, vsi1) + if err2 != nil { + require.Fail(t, err2.Error()) + } + fmt.Println(explanbilityStr2) + fmt.Println("---------------------------------------------------------------------------------------------------------------------------") + require.Equal(t, "No connection between Public Internet 161.26.0.0/16 and vsi1-ky[10.240.10.4]; "+ + "connection blocked by ingress\n\n", explanbilityStr2) + explanbilityStr3, err3 := vpcConfig.ExplainConnectivity(vsi1, cidr2) + if err3 != nil { + require.Fail(t, err3.Error()) + } + require.Equal(t, "The following connection exists between vsi1-ky[10.240.10.4] and Public Internet 161.26.0.0/32: "+ + "protocol: UDP; its enabled by\n"+ + "Egress Rules:\n~~~~~~~~~~~~~\nSecurityGroupLayer Rules\n------------------------\nenabling rules from sg1-ky:\n\t"+ + "index: 2, direction: outbound, conns: protocol: udp, dstPorts: 1-65535, cidr: 161.26.0.0/16\n\n", explanbilityStr3) + fmt.Println(explanbilityStr3) + fmt.Println("---------------------------------------------------------------------------------------------------------------------------") +} + +func TestGroupingExternal(t *testing.T) { + vpcConfig := getConfig(t, "input_sg_testing1_new.json") + if vpcConfig == nil { + require.Fail(t, "vpcConfig equals nil") + } + vsi1 := "vsi1-ky[10.240.10.4]" + cidr1 := "161.26.0.0/8" + explanbilityStr1, err1 := vpcConfig.ExplainConnectivity(vsi1, cidr1) + if err1 != nil { + require.Fail(t, err1.Error()) + } + require.Equal(t, "No connection between vsi1-ky[10.240.10.4] and Public Internet 161.0.0.0-161.25.255.255,161.27.0.0-161.255.255.255; "+ + "connection blocked by egress\n\n"+ + "The following connection exists between vsi1-ky[10.240.10.4] and Public Internet 161.26.0.0/16: protocol: UDP; its enabled by\n"+ + "Egress Rules:\n~~~~~~~~~~~~~\nSecurityGroupLayer Rules\n------------------------\nenabling rules from sg1-ky:\n"+ + "\tindex: 2, direction: outbound, conns: protocol: udp, dstPorts: 1-65535, cidr: 161.26.0.0/16\n\n", + explanbilityStr1) + fmt.Println(explanbilityStr1) + fmt.Println("---------------------------------------------------------------------------------------------------------------------------") + vsi2 := "vsi2-ky[10.240.20.4]" + cidrAll := "0.0.0.0/0" + explanbilityStr2, err2 := vpcConfig.ExplainConnectivity(vsi2, cidrAll) + if err2 != nil { + require.Fail(t, err2.Error()) + } + fmt.Println(explanbilityStr2) + fmt.Println("---------------------------------------------------------------------------------------------------------------------------") + require.Equal(t, "No connection between vsi2-ky[10.240.20.4] and Public Internet 0.0.0.0-141.255.255.255,143.0.0.0-255.255.255.255; "+ + "connection blocked by egress\n\n"+ + "The following connection exists between vsi2-ky[10.240.20.4] and Public Internet 142.0.0.0/8: protocol: ICMP; its enabled by\n"+ + "Egress Rules:\n~~~~~~~~~~~~~\nSecurityGroupLayer Rules\n------------------------\nenabling rules from sg2-ky:\n"+ + "\tindex: 3, direction: outbound, conns: protocol: icmp, icmpType: protocol: ICMP, cidr: 142.0.0.0/8\n\n", + explanbilityStr2) + explanbilityStr3, err3 := vpcConfig.ExplainConnectivity(cidrAll, vsi2) + if err3 != nil { + require.Fail(t, err3.Error()) + } + fmt.Println(explanbilityStr3) + require.Equal(t, "No connection between vsi2-ky[10.240.20.4] and Public Internet 0.0.0.0-141.255.255.255,143.0.0.0-255.255.255.255; "+ + "connection blocked by egress\n\nThe following connection exists between vsi2-ky[10.240.20.4] "+ + "and Public Internet 142.0.0.0/8: protocol: ICMP; its enabled by\nEgress Rules:\n"+ + "~~~~~~~~~~~~~\nSecurityGroupLayer Rules\n------------------------\nenabling rules from sg2-ky:\n\t"+ + "index: 3, direction: outbound, conns: protocol: icmp, icmpType: protocol: ICMP, cidr: 142.0.0.0/8\n\n", + explanbilityStr2) + fmt.Println("---------------------------------------------------------------------------------------------------------------------------") +} + // getConfigs returns map[string]*vpcmodel.VPCConfig obj for the input test (config json file) func getConfig(t *testing.T, inputConfig string) *vpcmodel.VPCConfig { inputConfigFile := filepath.Join(getTestsDir(), inputConfig) diff --git a/pkg/vpcmodel/grouping.go b/pkg/vpcmodel/grouping.go index 70e139b12..c48c846d5 100644 --- a/pkg/vpcmodel/grouping.go +++ b/pkg/vpcmodel/grouping.go @@ -22,8 +22,11 @@ type groupedNodesInfo struct { type groupedCommonProperties struct { conn *common.ConnectionSet connDiff *connectionDiff - // connStrKey is the string of conn per grouping of conn lines, and string of connDiff per grouping of diff lines - connStrKey string // the key used for grouping per connectivity lines or diff lines + rules *rulesConnection + // groupingStrKey is the key by which the grouping is done: + // the string of conn per grouping of conn lines, string of connDiff per grouping of diff lines + // and string of conn and rules for explainblity + groupingStrKey string // the key used for grouping per connectivity lines or diff lines } func (g *groupedNodesInfo) appendNode(n Node) { @@ -85,6 +88,18 @@ func newGroupConnLinesDiff(d *diffBetweenCfgs) (res *GroupConnLines, err error) return res, err } +func newGroupConnExplainability(c *VPCConfig, e *explainStruct) (res *GroupConnLines, err error) { + res = &GroupConnLines{ + c: c, + e: e, + srcToDst: newGroupingConnections(), + dstToSrc: newGroupingConnections(), + groupedEndpointsElemsMap: make(map[string]*groupedEndpointsElems), + groupedExternalNodesMap: make(map[string]*groupedExternalNodes)} + err = res.groupExternalAddressesForExplainability() + return res, err +} + // GroupConnLines used both for VPCConnectivity and for VPCsubnetConnectivity, one at a time. The other must be nil // todo: define abstraction above both? type GroupConnLines struct { @@ -92,6 +107,7 @@ type GroupConnLines struct { v *VPCConnectivity s *VPCsubnetConnectivity d *diffBetweenCfgs + e *explainStruct srcToDst *groupingConnections dstToSrc *groupingConnections // a map to groupedEndpointsElems used by GroupedConnLine from a unified key of such elements @@ -106,6 +122,7 @@ type GroupConnLines struct { // EndpointElem can be Node(networkInterface) / groupedExternalNodes / groupedNetworkInterfaces / NodeSet(subnet) type EndpointElem interface { Name() string + IsExternal() bool DrawioResourceIntf } @@ -116,14 +133,14 @@ type groupedConnLine struct { } func (g *groupedConnLine) String() string { - return g.src.Name() + " => " + g.dst.Name() + " : " + g.commonProperties.connStrKey + return g.src.Name() + " => " + g.dst.Name() + " : " + g.commonProperties.groupingStrKey } func (g *groupedConnLine) ConnLabel() string { if g.commonProperties.conn.AllowAll { return "" } - return g.commonProperties.connStrKey + return g.commonProperties.groupingStrKey } func (g *groupedConnLine) getSrcOrDst(isSrc bool) EndpointElem { @@ -183,7 +200,7 @@ func (g *GroupConnLines) getGroupedExternalNodes(grouped groupedExternalNodes) * } func (g *groupingConnections) addPublicConnectivity(ep EndpointElem, commonProps *groupedCommonProperties, targetNode Node) { - connKey := commonProps.connStrKey + connKey := commonProps.groupingStrKey if _, ok := (*g)[ep]; !ok { (*g)[ep] = map[string]*groupedNodesInfo{} } @@ -256,10 +273,12 @@ func (g *GroupConnLines) groupExternalAddresses(vsi bool) error { res := []*groupedConnLine{} for src, nodeConns := range allowedConnsCombined { for dst, conns := range nodeConns { - err := g.addLineToExternalGrouping(&res, conns.IsEmpty(), src, dst, - &groupedCommonProperties{conn: conns, connStrKey: conns.EnhancedString()}) - if err != nil { - return err + if !conns.IsEmpty() { + err := g.addLineToExternalGrouping(&res, src, dst, + &groupedCommonProperties{conn: conns, groupingStrKey: conns.EnhancedString()}) + if err != nil { + return err + } } } } @@ -282,11 +301,12 @@ func (g *GroupConnLines) groupExternalAddressesForDiff(thisMinusOther bool) erro for src, endpointConnDiff := range connRemovedChanged { for dst, connDiff := range endpointConnDiff { connDiffString := connDiffEncode(src, dst, connDiff) - connsEmpty := connDiff.conn1.IsEmpty() && connDiff.conn2.IsEmpty() - err := g.addLineToExternalGrouping(&res, connsEmpty, src, dst, - &groupedCommonProperties{connDiff: connDiff, connStrKey: connDiffString}) - if err != nil { - return err + if !(connDiff.conn1.IsEmpty() && connDiff.conn2.IsEmpty()) { + err := g.addLineToExternalGrouping(&res, src, dst, + &groupedCommonProperties{connDiff: connDiff, groupingStrKey: connDiffString}) + if err != nil { + return err + } } } } @@ -294,11 +314,27 @@ func (g *GroupConnLines) groupExternalAddressesForDiff(thisMinusOther bool) erro return nil } -func (g *GroupConnLines) addLineToExternalGrouping(res *[]*groupedConnLine, emptyConn bool, - src, dst VPCResourceIntf, commonProps *groupedCommonProperties) error { - if emptyConn { - return nil +// group public internet ranges for explainability lines +func (g *GroupConnLines) groupExternalAddressesForExplainability() error { + var res []*groupedConnLine + for _, rulesSrcDst := range *g.e { + connStr := "" + if rulesSrcDst.conn != nil { + connStr = rulesSrcDst.conn.String() + semicolon + } + groupingStrKey := connStr + rulesSrcDst.rules.rulesEncode(g.c) + err := g.addLineToExternalGrouping(&res, rulesSrcDst.src, rulesSrcDst.dst, + &groupedCommonProperties{conn: rulesSrcDst.conn, rules: rulesSrcDst.rules, groupingStrKey: groupingStrKey}) + if err != nil { + return err + } } + g.appendGrouped(res) + return nil +} + +func (g *GroupConnLines) addLineToExternalGrouping(res *[]*groupedConnLine, + src, dst VPCResourceIntf, commonProps *groupedCommonProperties) error { srcNode, srcIsNode := src.(Node) dstNode, dstIsNode := dst.(Node) if dst.IsExternal() && !dstIsNode || @@ -354,7 +390,7 @@ func (g *GroupConnLines) groupLinesByKey(srcGrouping, groupVsi bool) (res []*gro res = append(res, line) continue } - key := getKeyOfGroupConnLines(dstOrSrc, line.commonProperties.connStrKey) + key := getKeyOfGroupConnLines(dstOrSrc, line.commonProperties.groupingStrKey) if _, ok := groupingSrcOrDst[key]; !ok { groupingSrcOrDst[key] = []*groupedConnLine{} } @@ -495,11 +531,20 @@ func (g *groupedExternalNodes) String() string { // 2. connection of config1 // 3. connection of config2 // 4. info regarding missing endpoints: e.g. vsi0 removed -// -// this encoding prevents the need to change all the grouping datastuctures -// along the pipe: srcToDst and dstToSrc in addition to GroupedConnLine func connDiffEncode(src, dst VPCResourceIntf, connDiff *connectionDiff) string { conn1Str, conn2Str := conn1And2Str(connDiff) diffType, endpointsDiff := diffAndEndpointsDescription(connDiff.diff, src, dst, connDiff.thisMinusOther) return strings.Join([]string{diffType, conn1Str, conn2Str, endpointsDiff}, semicolon) } + +// encodes rulesConnection for grouping +func (rules *rulesConnection) rulesEncode(c *VPCConfig) string { + egressStr, ingressStr := "", "" + if len(rules.egressRules) > 0 { + egressStr = "egress:" + rules.egressRules.string(c) + semicolon + } + if len(rules.ingressRules) > 0 { + egressStr = "ingress:" + rules.ingressRules.string(c) + semicolon + } + return egressStr + ingressStr +} diff --git a/pkg/vpcmodel/groupingSelfLoop.go b/pkg/vpcmodel/groupingSelfLoop.go index e606ab498..1f829fba8 100644 --- a/pkg/vpcmodel/groupingSelfLoop.go +++ b/pkg/vpcmodel/groupingSelfLoop.go @@ -115,7 +115,7 @@ func (g *GroupConnLines) findMergeCandidates(groupingSrcOrDst map[string][]*grou bucketToKeys := make(map[string]map[string]struct{}) for _, key := range relevantKeys { lines := groupingSrcOrDst[key] - bucket := lines[0].commonProperties.connStrKey + bucket := lines[0].commonProperties.groupingStrKey subnetIfVsi := g.getSubnetIfVsi(lines[0].src) if subnetIfVsi != "" { bucket += ";" + subnetIfVsi @@ -332,7 +332,7 @@ func listOfUniqueEndpoints(oldGroupingSrcOrDst map[string][]*groupedConnLine, sr for _, line := range oldGroupingSrcOrDst[oldKeyToMerge] { endPointInKey := line.getSrcOrDst(!srcGrouping) if conn == "" { - conn = line.commonProperties.connStrKey // connection is the same for all lines to be merged + conn = line.commonProperties.groupingStrKey // connection is the same for all lines to be merged connProps = line.commonProperties } if _, isSliceEndpoints := endPointInKey.(*groupedEndpointsElems); isSliceEndpoints { diff --git a/pkg/vpcmodel/mdOutput.go b/pkg/vpcmodel/mdOutput.go index 3028a35bb..ad4994ceb 100644 --- a/pkg/vpcmodel/mdOutput.go +++ b/pkg/vpcmodel/mdOutput.go @@ -99,5 +99,5 @@ func connectivityLineMD(src, dst, conn string) string { } func getGroupedMDLine(line *groupedConnLine) string { - return connectivityLineMD(line.src.Name(), line.dst.Name(), line.commonProperties.connStrKey) + return connectivityLineMD(line.src.Name(), line.dst.Name(), line.commonProperties.groupingStrKey) } diff --git a/pkg/vpcmodel/nodesExplainability.go b/pkg/vpcmodel/nodesExplainability.go index 8faa40e1a..93507748f 100644 --- a/pkg/vpcmodel/nodesExplainability.go +++ b/pkg/vpcmodel/nodesExplainability.go @@ -2,17 +2,37 @@ package vpcmodel import ( "fmt" + "sort" + "strings" + + "github.com/np-guard/vpc-network-config-analyzer/pkg/common" ) // rulesInLayers contains specific rules across all layers (SGLayer/NACLLayer) type rulesInLayers map[string][]RulesInFilter -// RulesOfConnection contains the rules enabling a connection -type RulesOfConnection struct { +// rulesConnection contains the rules enabling a connection +type rulesConnection struct { ingressRules rulesInLayers egressRules rulesInLayers } +type rulesSingleSrcDst struct { + src Node + dst Node + conn *common.ConnectionSet + rules *rulesConnection +} + +type explainStruct []*rulesSingleSrcDst + +type explanation struct { + c *VPCConfig + explainStruct *explainStruct + // grouped connectivity result + groupedLines []*groupedConnLine +} + // finds the node of a given, by its name, Vsi func (c *VPCConfig) getVsiNode(name string) Node { for _, node := range c.Nodes { @@ -25,22 +45,117 @@ func (c *VPCConfig) getVsiNode(name string) Node { return nil } -// ExplainConnectivity todo: this will not be needed here once we connect explanbility to the cli -// todo: add support of external network -func (c *VPCConfig) ExplainConnectivity(srcName, dstName string) (explanation string, err error) { - src := c.getVsiNode(srcName) - if src == nil { - return "", fmt.Errorf("src %v does not represent a VSI", srcName) +// given input cidr, gets (disjoint) external nodes I s.t.: +// 1. The union of these nodes is the cidr +// 2. Let i be a node in I and n be a node in VPCConfig. +// i and n are either disjoint or i is contained in n +// Note that the vpconfig nodes were chosen w.r.t. connectivity rules (SG and NACL) +// s.t. each node either fully belongs to a rule or is disjoint to it. +// to get nodes I as above: +// 1. Calculate the IP blocks of the nodes N +// 2. Calculate from N and the cidr block, disjoint IP blocks +// 3. Return the nodes created from each block from 2 contained in the input cidr +func (c *VPCConfig) getCidrExternalNodes(cidr string) (cidrDisjointNodes []Node, err error) { + cidrsIPBlock := common.NewIPBlockFromCidr(cidr) + if cidrsIPBlock == nil { // string cidr does not represent a legal cidr + return nil, nil } - dst := c.getVsiNode(dstName) - if dst == nil { - return "", fmt.Errorf("dst %v does not represent a VSI", dstName) + cidrIPBlocks := []*common.IPBlock{cidrsIPBlock} + // 1. + vpcConfigNodesExternalBlock := make([]*common.IPBlock, 0) + for _, node := range c.Nodes { + if !node.IsExternal() { + continue + } + thisNodeBlock := common.NewIPBlockFromCidr(node.Cidr()) + vpcConfigNodesExternalBlock = append(vpcConfigNodesExternalBlock, thisNodeBlock) + } + // 2. + disjointBlocks := common.DisjointIPBlocks(cidrIPBlocks, vpcConfigNodesExternalBlock) + // 3. + cidrDisjointNodes = make([]Node, 0) + for _, block := range disjointBlocks { + if block.ContainedIn(cidrsIPBlock) { + node, err1 := newExternalNode(true, block) + if err1 != nil { + return nil, err1 + } + cidrDisjointNodes = append(cidrDisjointNodes, node) + } + } + return cidrDisjointNodes, nil +} + +// given a string or a vsi or a cidr returns the corresponding node(s) +func (c *VPCConfig) getNodesFromInput(cidrOrName string) ([]Node, error) { + if vsi := c.getVsiNode(cidrOrName); vsi != nil { + return []Node{vsi}, nil } - rulesOfConnection, err1 := c.GetRulesOfConnection(src, dst) + return c.getCidrExternalNodes(cidrOrName) +} + +// todo: group results. for now just prints each + +// ExplainConnectivity todo: this will not be needed here once we connect explanbility to the cli +// todo: support vsi given as an ID/IP address (CRN?) +func (c *VPCConfig) ExplainConnectivity(src, dst string) (out string, err error) { + explanationStruct, err1 := c.computeExplainRules(src, dst) if err1 != nil { return "", err1 } - return rulesOfConnection.String(src, dst, c) + err2 := explanationStruct.computeConnections(c) + if err2 != nil { + return "", err2 + } + groupedLines, err3 := newGroupConnExplainability(c, &explanationStruct) + if err3 != nil { + return "", err3 + } + res := &explanation{c, &explanationStruct, groupedLines.GroupedLines} + return res.String(), nil +} + +func (c *VPCConfig) computeExplainRules(srcName, dstName string) (explanationStruct explainStruct, err error) { + srcNodes, dstNodes, err := c.processInput(srcName, dstName) + if err != nil { + return nil, err + } + explanationStruct = make(explainStruct, max(len(srcNodes), len(dstNodes))) + i := 0 + for _, src := range srcNodes { + for _, dst := range dstNodes { + rulesOfConnection, err := c.getRulesOfConnection(src, dst) + if err != nil { + return nil, err + } + rulesThisSrcDst := &rulesSingleSrcDst{src, dst, nil, rulesOfConnection} + explanationStruct[i] = rulesThisSrcDst + i++ + } + } + return explanationStruct, nil +} + +func (c *VPCConfig) processInput(srcName, dstName string) (srcNodes, dstNodes []Node, err error) { + srcNodes, err = c.getNodesFromInput(srcName) + if err != nil { + return nil, nil, err + } + if len(srcNodes) == 0 { + return nil, nil, fmt.Errorf("src %v does not represent a VSI or an external IP", srcName) + } + dstNodes, err = c.getNodesFromInput(dstName) + if err != nil { + return nil, nil, err + } + if len(dstNodes) == 0 { + return nil, nil, fmt.Errorf("dst %v does not represent a VSI or an external IP", dstName) + } + // only one of src/dst can be external; there could be multiple nodes only if external + if srcNodes[0].IsExternal() && dstNodes[0].IsExternal() { + return nil, nil, fmt.Errorf("both src %v and dst %v are external", srcName, dstName) + } + return srcNodes, dstNodes, nil } func (c *VPCConfig) getFiltersEnablingRulesBetweenNodesPerDirectionAndLayer( @@ -56,28 +171,31 @@ func (c *VPCConfig) getFiltersEnablingRulesBetweenNodesPerDirectionAndLayer( return &rulesOfFilter, nil } -func (c *VPCConfig) GetRulesOfConnection(src, dst Node) (rulesOfConnection *RulesOfConnection, err error) { +func (c *VPCConfig) getRulesOfConnection(src, dst Node) (rulesOfConnection *rulesConnection, err error) { filterLayers := []string{SecurityGroupLayer} - rulesOfConnection = &RulesOfConnection{make(rulesInLayers, len(filterLayers)), - make(rulesInLayers, len(filterLayers))} + rulesOfConnection = &rulesConnection{} ingressRulesPerLayer, egressRulesPerLayer := make(rulesInLayers), make(rulesInLayers) for _, layer := range filterLayers { - // ingress rules - ingressRules, err1 := c.getFiltersEnablingRulesBetweenNodesPerDirectionAndLayer(src, dst, true, layer) - if err1 != nil { - return nil, err1 - } - if len(*ingressRules) > 0 { - ingressRulesPerLayer[layer] = *ingressRules + // ingress rules: relevant only if dst is internal + if dst.IsInternal() { + ingressRules, err1 := c.getFiltersEnablingRulesBetweenNodesPerDirectionAndLayer(src, dst, true, layer) + if err1 != nil { + return nil, err1 + } + if len(*ingressRules) > 0 { + ingressRulesPerLayer[layer] = *ingressRules + } } - // egress rules - egressRules, err2 := c.getFiltersEnablingRulesBetweenNodesPerDirectionAndLayer(src, dst, false, layer) - if err2 != nil { - return nil, err2 - } - if len(*egressRules) > 0 { - egressRulesPerLayer[layer] = *egressRules + // egress rules: relevant only is src is internal + if src.IsInternal() { + egressRules, err2 := c.getFiltersEnablingRulesBetweenNodesPerDirectionAndLayer(src, dst, false, layer) + if err2 != nil { + return nil, err2 + } + if len(*egressRules) > 0 { + egressRulesPerLayer[layer] = *egressRules + } } } rulesOfConnection.ingressRules = ingressRulesPerLayer @@ -85,37 +203,125 @@ func (c *VPCConfig) GetRulesOfConnection(src, dst Node) (rulesOfConnection *Rule return rulesOfConnection, nil } -// todo: when there is more than just SG, add explanation when all layers are default +// node is from getCidrExternalNodes, thus there is a node in VPCConfig that either equal to or contains it. +func (c *VPCConfig) getContainingConfigNode(node Node) (Node, error) { + if !node.IsExternal() { // node is not external - nothing to do + return node, nil + } + nodeIPBlock := common.NewIPBlockFromCidr(node.Cidr()) + if nodeIPBlock == nil { // string cidr does not represent a legal cidr + return nil, fmt.Errorf("could not find IP block of external node %v", node.Name()) + } + for _, configNode := range c.Nodes { + if !configNode.IsExternal() { + continue + } + configNodeIPBlock := common.NewIPBlockFromCidr(configNode.Cidr()) + if nodeIPBlock.ContainedIn(configNodeIPBlock) { + return configNode, nil + } + } + return nil, fmt.Errorf("could not find containing config node for %v", node.Name()) +} + +// prints each separately without grouping - for debug +func (explanationStruct *explainStruct) String(c *VPCConfig) (string, error) { + resStr := "" + for _, rulesSrcDst := range *explanationStruct { + resStr += stringExplainabilityLine(c, rulesSrcDst.src, rulesSrcDst.dst, rulesSrcDst.conn, rulesSrcDst.rules) + } + return resStr, nil +} + +func (explanation *explanation) String() string { + linesStr := make([]string, len(explanation.groupedLines)) + groupedLines := explanation.groupedLines + for i, line := range groupedLines { + linesStr[i] = stringExplainabilityLine(explanation.c, line.src, line.dst, line.commonProperties.conn, + line.commonProperties.rules) + } + sort.Strings(linesStr) + return strings.Join(linesStr, "\n") + "\n" +} -func (rulesOfConnection *RulesOfConnection) String(src, dst Node, c *VPCConfig) (string, error) { - noIngressRules := len(rulesOfConnection.ingressRules) == 0 - noEgressRules := len(rulesOfConnection.egressRules) == 0 - egressRulesStr := rulesOfConnection.egressRules.string(c) - ingressRulesStr := rulesOfConnection.ingressRules.string(c) +func stringExplainabilityLine(c *VPCConfig, src, dst EndpointElem, conn *common.ConnectionSet, rules *rulesConnection) string { + needEgress := !src.IsExternal() + needIngress := !dst.IsExternal() + noIngressRules := len(rules.ingressRules) == 0 && needIngress + noEgressRules := len(rules.egressRules) == 0 && needEgress + egressRulesStr := fmt.Sprintf("Egress Rules:\n~~~~~~~~~~~~~\n%v", rules.egressRules.string(c)) + ingressRulesStr := fmt.Sprintf("Ingress Rules:\n~~~~~~~~~~~~~~\n%v", rules.ingressRules.string(c)) + noConnection := fmt.Sprintf("No connection between %v and %v;", src.Name(), dst.Name()) + resStr := "" switch { case noIngressRules && noEgressRules: - return fmt.Sprintf("No connection between %v and %v; connection blocked both by ingress and egress\n", src.Name(), dst.Name()), nil + resStr += fmt.Sprintf("%v connection blocked both by ingress and egress\n", noConnection) case noIngressRules: - return fmt.Sprintf("No connection between %v and %v; connection blocked by ingress\n"+ - "Egress Rules:\n~~~~~~~~~~~~~~\n%v", src.Name(), dst.Name(), egressRulesStr), nil + resStr += fmt.Sprintf("%v connection blocked by ingress\n", noConnection) + if needEgress { + resStr += egressRulesStr + } case noEgressRules: - return fmt.Sprintf("No connection between %v and %v; connection blocked by egress\n"+ - "Ingress Rules:\n~~~~~~~~~~~~~\n%v", src.Name(), dst.Name(), ingressRulesStr), nil + resStr += fmt.Sprintf("%v connection blocked by egress\n", noConnection) + if needIngress { + resStr += ingressRulesStr + } default: // there is a connection - // todo: connectivity is computed for the entire network, even though we need only src-> dst - // this is seems the time spent here should be neglectable, not worth the effort of adding dedicated code. - connectivity, err := c.GetVPCNetworkConnectivity(false) // computes connectivity - if err != nil { - return "", err + resStr = fmt.Sprintf("The following connection exists between %v and %v: %v; its enabled by\n", src.Name(), dst.Name(), + conn.String()) + if needEgress { + resStr += egressRulesStr } - conn, ok := connectivity.AllowedConnsCombined[src][dst] - if !ok { - return "", fmt.Errorf("error: there is a connection between %v and %v, but connection computation failed", - src.Name(), dst.Name()) + if needIngress { + resStr += ingressRulesStr + } + } + return resStr +} + +// todo: connectivity is computed for the entire network, even though we need only for specific src, dst pairs +// this is seems the time spent here should be neglectable, not worth the effort of adding dedicated code. +func (explanationStruct *explainStruct) computeConnections(c *VPCConfig) error { + connectivity, err := c.GetVPCNetworkConnectivity(false) // computes connectivity + if err != nil { + return err + } + for _, rulesSrcDst := range *explanationStruct { + // is there a connection? + if (len(rulesSrcDst.rules.egressRules) > 0 || rulesSrcDst.src.IsExternal()) && // egress enabled or not needed + (len(rulesSrcDst.rules.ingressRules) > 0 || rulesSrcDst.dst.IsExternal()) { // ingress enabled or not needed + conn, err := connectivity.getConnection(c, rulesSrcDst.src, rulesSrcDst.dst) + if err != nil { + return err + } + rulesSrcDst.conn = conn } - return fmt.Sprintf("The following connection exists between %v and %v: %v; its enabled by\nEgress Rules:\n~~~~~~~~~~~~~\n%v\n"+ - "Ingress Rules:\n~~~~~~~~~~~~~~\n%v\n", src.Name(), dst.Name(), conn.String(), egressRulesStr, ingressRulesStr), nil } + return nil +} + +// given that there is a connection between src to dst, gets it +// if src or dst is a node then the node is from getCidrExternalNodes, +// thus there is a node in VPCConfig that either equal to or contains it. +func (v *VPCConnectivity) getConnection(c *VPCConfig, src, dst Node) (conn *common.ConnectionSet, err error) { + srcForConnection, err1 := c.getContainingConfigNode(src) + if err1 != nil { + return nil, err1 + } + dstForConnection, err2 := c.getContainingConfigNode(dst) + if err2 != nil { + return nil, err2 + } + var ok bool + srcMapValue, ok := v.AllowedConnsCombined[srcForConnection] + if ok { + conn, ok = srcMapValue[dstForConnection] + } + if !ok { + return nil, fmt.Errorf("error: there is a connection between %v and %v, but connection computation failed", + srcForConnection.Name(), dstForConnection.Name()) + } + return conn, nil } func (rulesInLayers *rulesInLayers) string(c *VPCConfig) string {