From d29e257bed1a7eaad229f9a3dfef1b1b8af47b5b Mon Sep 17 00:00:00 2001 From: ShiriMoran <139739065+ShiriMoran@users.noreply.github.com> Date: Wed, 10 Jan 2024 09:34:10 +0200 Subject: [PATCH] 302 explainabilty query connection (#312) * added option to query connection with relevant tests --- .../examples/input_sg_testing1_new.json | 22 ++ pkg/ibmvpc/explainability_test.go | 278 +++++++++++++----- pkg/ibmvpc/sg_analysis.go | 34 ++- pkg/ibmvpc/vpc.go | 15 +- pkg/vpcmodel/abstractVPC.go | 2 +- pkg/vpcmodel/nodesExplainability.go | 58 ++-- 6 files changed, 300 insertions(+), 109 deletions(-) diff --git a/pkg/ibmvpc/examples/input_sg_testing1_new.json b/pkg/ibmvpc/examples/input_sg_testing1_new.json index 7115dc1e3..5a1f18aca 100644 --- a/pkg/ibmvpc/examples/input_sg_testing1_new.json +++ b/pkg/ibmvpc/examples/input_sg_testing1_new.json @@ -999,6 +999,28 @@ "remote": { "cidr_block": "10.240.30.0/24" } + }, + { + "direction": "outbound", + "href": "href:124", + "id": "id:125", + "ip_version": "ipv4", + "protocol": "tcp", + "remote": { + "cidr_block": "0.0.0.0/0" + } + }, + { + "direction": "outbound", + "href": "href:124", + "id": "id:125", + "ip_version": "ipv4", + "protocol": "tcp", + "port_max": 200, + "port_min": 100, + "remote": { + "cidr_block": "0.0.0.0/0" + } } ], "tags": [], diff --git a/pkg/ibmvpc/explainability_test.go b/pkg/ibmvpc/explainability_test.go index 8a1e99192..464a6e57a 100644 --- a/pkg/ibmvpc/explainability_test.go +++ b/pkg/ibmvpc/explainability_test.go @@ -9,16 +9,37 @@ import ( "github.com/stretchr/testify/require" + "github.com/np-guard/vpc-network-config-analyzer/pkg/common" "github.com/np-guard/vpc-network-config-analyzer/pkg/vpcmodel" ) -// todo: quick and dirty tmp until added to the cli, by which these will be added as end-to-end tests +// getConfigs returns map[string]*vpcmodel.VPCConfig obj for the input test (config json file) +func getConfig(t *testing.T) *vpcmodel.VPCConfig { + inputConfigFile := filepath.Join(getTestsDir(), "input_sg_testing1_new.json") + inputConfigContent, err := os.ReadFile(inputConfigFile) + if err != nil { + t.Fatalf("err: %s", err) + } + rc, err := ParseResources(inputConfigContent) + if err != nil { + t.Fatalf("err: %s", err) + } + vpcConfigs, err := VPCConfigsFromResources(rc, "", false) + if err != nil { + t.Fatalf("err: %s", err) + } + for _, vpcConfig := range vpcConfigs { + return vpcConfig + } + return nil +} + func TestVsiToVsi(t *testing.T) { - vpcConfig := getConfig(t, "input_sg_testing1_new.json") + vpcConfig := getConfig(t) if vpcConfig == nil { require.Fail(t, "vpcConfig equals nil") } - explanbilityStr1, err1 := vpcConfig.ExplainConnectivity("vsi2-ky[10.240.20.4]", "vsi3b-ky[10.240.30.4]") + explanbilityStr1, err1 := vpcConfig.ExplainConnectivity("vsi2-ky[10.240.20.4]", "vsi3b-ky[10.240.30.4]", nil) if err1 != nil { require.Fail(t, err1.Error()) } @@ -31,7 +52,7 @@ func TestVsiToVsi(t *testing.T) { "\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]") + explanbilityStr2, err2 := vpcConfig.ExplainConnectivity("vsi2-ky[10.240.20.4]", "vsi1-ky[10.240.10.4]", nil) if err2 != nil { require.Fail(t, err2.Error()) } @@ -42,18 +63,19 @@ func TestVsiToVsi(t *testing.T) { "\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]") + explanbilityStr3, err3 := vpcConfig.ExplainConnectivity("vsi3a-ky[10.240.30.5]", "vsi1-ky[10.240.10.4]", nil) if err3 != nil { require.Fail(t, err3.Error()) } fmt.Println(explanbilityStr3) 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\nIngress Rules:\n~~~~~~~~~~~~~~\nSecurityGroupLayer Rules"+ - "\n------------------------\nenabling rules from sg1-ky:\n"+ + "All Connections; its enabled by\nEgress Rules:\n~~~~~~~~~~~~~\nSecurityGroupLayer Rules\n------------------------\n"+ + "enabling rules from sg3-ky:\n\tindex: 0, direction: outbound, protocol: all, cidr: 0.0.0.0/0\n"+ + "\tindex: 2, direction: outbound, conns: protocol: tcp, dstPorts: 1-65535, cidr: 0.0.0.0/0\n"+ + "\tindex: 3, direction: outbound, conns: protocol: tcp, dstPorts: 100-200, cidr: 0.0.0.0/0\n"+ + "Ingress 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]") + explanbilityStr4, err4 := vpcConfig.ExplainConnectivity("vsi1-ky[10.240.10.4]", "vsi2-ky[10.240.20.4]", nil) if err4 != nil { require.Fail(t, err4.Error()) } @@ -61,81 +83,48 @@ func TestVsiToVsi(t *testing.T) { 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\n", explanbilityStr4) - explanbilityStr5, err5 := vpcConfig.ExplainConnectivity("vsi3a-ky[10.240.30.5]", "vsi2-ky[10.240.20.4]") + explanbilityStr5, err5 := vpcConfig.ExplainConnectivity("vsi3a-ky[10.240.30.5]", "vsi2-ky[10.240.20.4]", nil) 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\n", explanbilityStr5) - fmt.Println("done") -} - -// sg1-ky: vsi1-ky -// sg2-ky: vsi2-ky, vsi3b-ky -// sg3-ky: vsi3a-ky -// sg1-ky, sg3-ky: default -// sg2-ky: allow all -func TestSGDefaultRules(t *testing.T) { - vpcConfig := getConfig(t, "input_sg_testing_default.json") - if vpcConfig == nil { - require.Fail(t, "vpcConfig equals nil") - } - // no connection, disabled by default rules - explanbilityStr1, err1 := vpcConfig.ExplainConnectivity("vsi1-ky[10.240.10.4]", "vsi3a-ky[10.240.30.5]") - if err1 != nil { - require.Fail(t, err1.Error()) - } - 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"+ - "------------------------\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\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 { - require.Fail(t, err2.Error()) - } - fmt.Println(explanbilityStr2) - 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"+ - "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) + require.Equal(t, "No connection between vsi3a-ky[10.240.30.5] and vsi2-ky[10.240.20.4]; "+ + "connection blocked by ingress\nEgress Rules:\n~~~~~~~~~~~~~\nSecurityGroupLayer Rules\n------------------------\n"+ + "enabling rules from sg3-ky:\n\tindex: 0, direction: outbound, protocol: all, cidr: 0.0.0.0/0\n"+ + "\tindex: 2, direction: outbound, conns: protocol: tcp, dstPorts: 1-65535, cidr: 0.0.0.0/0\n"+ + "\tindex: 3, direction: outbound, conns: protocol: tcp, dstPorts: 100-200, cidr: 0.0.0.0/0\n\n", explanbilityStr5) fmt.Println("done") } func TestInputValidity(t *testing.T) { - vpcConfig := getConfig(t, "input_sg_testing1_new.json") + vpcConfig := getConfig(t) 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) + _, err1 := vpcConfig.ExplainConnectivity(cidr1, cidr2, nil) fmt.Println(err1.Error()) if err1 == nil { require.Fail(t, err1.Error()) } - _, err2 := vpcConfig.ExplainConnectivity(cidr1, nonExistingVSI) + _, err2 := vpcConfig.ExplainConnectivity(cidr1, nonExistingVSI, nil) 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") +func TestSimpleExternalSG(t *testing.T) { + vpcConfig := getConfig(t) 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) + explanbilityStr1, err1 := vpcConfig.ExplainConnectivity(vsi1, cidr1, nil) if err1 != nil { require.Fail(t, err1.Error()) } @@ -145,7 +134,7 @@ func TestSimpleExternal(t *testing.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) + explanbilityStr2, err2 := vpcConfig.ExplainConnectivity(cidr1, vsi1, nil) if err2 != nil { require.Fail(t, err2.Error()) } @@ -153,7 +142,7 @@ func TestSimpleExternal(t *testing.T) { 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) + explanbilityStr3, err3 := vpcConfig.ExplainConnectivity(vsi1, cidr2, nil) if err3 != nil { require.Fail(t, err3.Error()) } @@ -165,14 +154,14 @@ func TestSimpleExternal(t *testing.T) { fmt.Println("---------------------------------------------------------------------------------------------------------------------------") } -func TestGroupingExternal(t *testing.T) { - vpcConfig := getConfig(t, "input_sg_testing1_new.json") +func TestGroupingExternalSG(t *testing.T) { + vpcConfig := getConfig(t) 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) + explanbilityStr1, err1 := vpcConfig.ExplainConnectivity(vsi1, cidr1, nil) if err1 != nil { require.Fail(t, err1.Error()) } @@ -186,7 +175,7 @@ func TestGroupingExternal(t *testing.T) { fmt.Println("---------------------------------------------------------------------------------------------------------------------------") vsi2 := "vsi2-ky[10.240.20.4]" cidrAll := "0.0.0.0/0" - explanbilityStr2, err2 := vpcConfig.ExplainConnectivity(vsi2, cidrAll) + explanbilityStr2, err2 := vpcConfig.ExplainConnectivity(vsi2, cidrAll, nil) if err2 != nil { require.Fail(t, err2.Error()) } @@ -198,7 +187,7 @@ func TestGroupingExternal(t *testing.T) { "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) + explanbilityStr3, err3 := vpcConfig.ExplainConnectivity(cidrAll, vsi2, nil) if err3 != nil { require.Fail(t, err3.Error()) } @@ -212,23 +201,158 @@ func TestGroupingExternal(t *testing.T) { 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) - inputConfigContent, err := os.ReadFile(inputConfigFile) - if err != nil { - t.Fatalf("err: %s", err) +func TestQueryConnectionSGBasic(t *testing.T) { + vpcConfig := getConfig(t) + if vpcConfig == nil { + require.Fail(t, "vpcConfig equals nil") } - rc, err := ParseResources(inputConfigContent) - if err != nil { - t.Fatalf("err: %s", err) + // test1: a connection exists, but it is not the required one by query + explanbilityStr1, err1 := vpcConfig.ExplainConnectivity("vsi2-ky[10.240.20.4]", "vsi3b-ky[10.240.30.4]", common.NewConnectionSet(true)) + if err1 != nil { + require.Fail(t, err1.Error()) } - vpcConfigs, err := VPCConfigsFromResources(rc, "", false) - if err != nil { - t.Fatalf("err: %s", err) + require.Equal(t, "There is no connection \"All Connections\" between vsi2-ky[10.240.20.4] and vsi3b-ky[10.240.30.4]; "+ + "connection blocked by ingress\nEgress Rules:\n~~~~~~~~~~~~~\nSecurityGroupLayer Rules\n------------------------\n"+ + "enabling 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\n", explanbilityStr1) + fmt.Println(explanbilityStr1) + fmt.Println("---------------------------------------------------------------------------------------------------------------------------") + // test2: the existing connection is exactly the one required by the query + vsi1 := "vsi1-ky[10.240.10.4]" + cidr1 := "161.26.0.0/16" + connectionUDP1 := common.NewConnectionSet(false) + connectionUDP1.AddTCPorUDPConn(common.ProtocolUDP, common.MinPort, common.MaxPort, common.MinPort, common.MaxPort) + explanbilityStr2, err2 := vpcConfig.ExplainConnectivity(vsi1, cidr1, connectionUDP1) + if err2 != nil { + require.Fail(t, err2.Error()) } - for _, vpcConfig := range vpcConfigs { - return vpcConfig + require.Equal(t, "Connection protocol: UDP exists between vsi1-ky[10.240.10.4] and Public Internet 161.26.0.0/16; 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", + explanbilityStr2) + fmt.Println(explanbilityStr2) + fmt.Println("---------------------------------------------------------------------------------------------------------------------------") + + //test3: the required connection is contained in the existing one per connection + connectionUDP2 := common.NewConnectionSet(false) + connectionUDP2.AddTCPorUDPConn(common.ProtocolUDP, 10, 100, 443, 443) + explanbilityStr3, err3 := vpcConfig.ExplainConnectivity(vsi1, cidr1, connectionUDP2) + if err3 != nil { + require.Fail(t, err3.Error()) } - return nil + require.Equal(t, "Connection protocol: UDP src-ports: 10-100 dst-ports: 443 exists between vsi1-ky[10.240.10.4] "+ + "and Public Internet 161.26.0.0/16; 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", + explanbilityStr3) + fmt.Println(explanbilityStr3) + fmt.Println("---------------------------------------------------------------------------------------------------------------------------") + // test4: the required connection is contained in the existing one per ip of src/dst + cidr2 := "161.26.0.0/20" + explanbilityStr4, err4 := vpcConfig.ExplainConnectivity(vsi1, cidr2, connectionUDP2) + if err4 != nil { + require.Fail(t, err4.Error()) + } + require.Equal(t, "Connection protocol: UDP src-ports: 10-100 dst-ports: 443 exists between vsi1-ky[10.240.10.4] "+ + "and Public Internet 161.26.0.0/20; 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", + explanbilityStr4) + fmt.Println(explanbilityStr4) + fmt.Println("---------------------------------------------------------------------------------------------------------------------------") + // test5: the required connection exists for part of the dst ip + cidr3 := "161.26.0.0/12" + explanbilityStr5, err5 := vpcConfig.ExplainConnectivity(vsi1, cidr3, connectionUDP2) + if err5 != nil { + require.Fail(t, err5.Error()) + } + require.Equal(t, "Connection protocol: UDP src-ports: 10-100 dst-ports: "+ + "443 exists between vsi1-ky[10.240.10.4] and Public Internet 161.26.0.0/16; 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"+ + "There is no connection \"protocol: UDP src-ports: 10-100 dst-ports: 443\" "+ + "between vsi1-ky[10.240.10.4] and Public Internet 161.16.0.0-161.25.255.255,161.27.0.0-161.31.255.255; "+ + "connection blocked by egress\n\n", explanbilityStr5) + fmt.Println(explanbilityStr5) + // test6: a connection does not exist regardless of the query + explanbilityStr6, err6 := vpcConfig.ExplainConnectivity("vsi1-ky[10.240.10.4]", "vsi3a-ky[10.240.30.5]", connectionUDP2) + if err6 != nil { + require.Fail(t, err6.Error()) + } + require.Equal(t, "There is no connection \"protocol: UDP src-ports: 10-100 dst-ports: 443\" "+ + "between vsi1-ky[10.240.10.4] and vsi3a-ky[10.240.30.5]; "+ + "connection blocked both by ingress and egress\n\n", explanbilityStr6) + fmt.Println(explanbilityStr6) + fmt.Println("---------------------------------------------------------------------------------------------------------------------------") +} + +func TestQueryConnectionSGRules(t *testing.T) { + vpcConfig := getConfig(t) + if vpcConfig == nil { + require.Fail(t, "vpcConfig equals nil") + } + // test1: all rules are relevant (for comparison) + vsi1 := "vsi1-ky[10.240.10.4]" + vsi3a := "vsi3a-ky[10.240.30.5]" + explanbilityStr1, err1 := vpcConfig.ExplainConnectivity(vsi3a, vsi1, nil) + if err1 != nil { + require.Fail(t, err1.Error()) + } + fmt.Println(explanbilityStr1) + 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\nEgress Rules:\n~~~~~~~~~~~~~\nSecurityGroupLayer Rules\n------------------------\n"+ + "enabling rules from sg3-ky:\n\tindex: 0, direction: outbound, protocol: all, cidr: 0.0.0.0/0\n"+ + "\tindex: 2, direction: outbound, conns: protocol: tcp, dstPorts: 1-65535, cidr: 0.0.0.0/0\n"+ + "\tindex: 3, direction: outbound, conns: protocol: tcp, dstPorts: 100-200, cidr: 0.0.0.0/0\n"+ + "Ingress 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", explanbilityStr1) + // test 2: only a subset of the rules are relevant, protocol wise + connectionUDP1 := common.NewConnectionSet(false) + connectionUDP1.AddTCPorUDPConn(common.ProtocolUDP, common.MinPort, common.MaxPort, common.MinPort, common.MaxPort) + explanbilityStr2, err2 := vpcConfig.ExplainConnectivity(vsi3a, vsi1, connectionUDP1) + if err2 != nil { + require.Fail(t, err2.Error()) + } + fmt.Println(explanbilityStr2) + require.Equal(t, "Connection protocol: UDP exists between vsi3a-ky[10.240.30.5] and vsi1-ky[10.240.10.4]; 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\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", explanbilityStr2) + fmt.Println("---------------------------------------------------------------------------------------------------------------------------") + + // test 3: only a subset of the rules are relevant, port wise and protocol wise + connectionTCP1 := common.NewConnectionSet(false) + connectionTCP1.AddTCPorUDPConn(common.ProtocolTCP, common.MinPort, common.MaxPort, 50, 54) + explanbilityStr3, err3 := vpcConfig.ExplainConnectivity(vsi3a, vsi1, connectionTCP1) + if err3 != nil { + require.Fail(t, err3.Error()) + } + fmt.Println(explanbilityStr3) + require.Equal(t, "Connection protocol: TCP dst-ports: 50-54 exists between vsi3a-ky[10.240.30.5] and vsi1-ky[10.240.10.4]; "+ + "its enabled by\nEgress Rules:\n~~~~~~~~~~~~~\nSecurityGroupLayer Rules\n------------------------\n"+ + "enabling rules from sg3-ky:\n"+ + "\tindex: 0, direction: outbound, protocol: all, cidr: 0.0.0.0/0\n"+ + "\tindex: 2, direction: outbound, conns: protocol: tcp, dstPorts: 1-65535, cidr: 0.0.0.0/0\n"+ + "Ingress 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) + fmt.Println("---------------------------------------------------------------------------------------------------------------------------") + + // test 4: all rules are relevant, with specified port wise protocol + connectionTCP2 := common.NewConnectionSet(false) + connectionTCP2.AddTCPorUDPConn(common.ProtocolTCP, common.MinPort, common.MaxPort, 120, 230) + explanbilityStr4, err4 := vpcConfig.ExplainConnectivity(vsi3a, vsi1, connectionTCP2) + if err4 != nil { + require.Fail(t, err4.Error()) + } + fmt.Println(explanbilityStr4) + require.Equal(t, "Connection protocol: TCP dst-ports: 120-230 exists between vsi3a-ky[10.240.30.5] and vsi1-ky[10.240.10.4]; "+ + "its enabled by\nEgress Rules:\n~~~~~~~~~~~~~\nSecurityGroupLayer Rules\n------------------------\n"+ + "enabling rules from sg3-ky:\n\tindex: 0, direction: outbound, protocol: all, cidr: 0.0.0.0/0\n"+ + "\tindex: 2, direction: outbound, conns: protocol: tcp, dstPorts: 1-65535, cidr: 0.0.0.0/0\n\t"+ + "index: 3, direction: outbound, conns: protocol: tcp, dstPorts: 100-200, 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", explanbilityStr4) + fmt.Println("---------------------------------------------------------------------------------------------------------------------------") } diff --git a/pkg/ibmvpc/sg_analysis.go b/pkg/ibmvpc/sg_analysis.go index 1527a3b61..8397e7054 100644 --- a/pkg/ibmvpc/sg_analysis.go +++ b/pkg/ibmvpc/sg_analysis.go @@ -2,6 +2,7 @@ package ibmvpc import ( "fmt" + "slices" "sort" "strings" @@ -322,15 +323,40 @@ func (sga *SGAnalyzer) AllowedConnectivity(target string, isIngress bool) *commo return vpcmodel.NoConns() } -// rulesInConnectivity list of SG rules contributing to the connectivity -func (sga *SGAnalyzer) rulesInConnectivity(target string, isIngress bool) []int { +// rulesInConnectivity list of SG rules contributing to the connectivity, if the required connection exists +// 1. The required connection (src/dst) is detected, if exists. +// 2. If connection is part of the query: is the required connection contained in the existing connection? +// if it does, then the contributing rules are detected: rules that intersect the required connection +// otherwise, the answer to the query is "no" and nil is returned +func (sga *SGAnalyzer) rulesInConnectivity(target string, conn *common.ConnectionSet, isIngress bool) ([]int, error) { analyzedConns, ipb := sga.getAnalyzedConnsIPB(target, isIngress) for definedTarget, rules := range analyzedConns.contribRules { if ipb.ContainedIn(definedTarget) { - return rules + if conn != nil { // connection is part of the query + contained, err := conn.ContainedIn(analyzedConns.allowedconns[definedTarget]) + if err != nil { + return nil, err + } + if contained { + return sga.getRulesRelevantConn(rules, conn) + } + return nil, nil + } + return rules, nil // connection not part of query - all rules are relevant } } - return nil + return nil, nil +} + +// given a list of rules and a connection, return the sublist of rules that contributes to the connection +func (sga *SGAnalyzer) getRulesRelevantConn(rules []int, conn *common.ConnectionSet) ([]int, error) { + relevantRules := []int{} + for _, rule := range append(sga.ingressRules, sga.egressRules...) { + if slices.Contains(rules, rule.index) && !conn.Intersection(rule.connections).IsEmpty() { + relevantRules = append(relevantRules, rule.index) + } + } + return relevantRules, nil } func (sga *SGAnalyzer) getAnalyzedConnsIPB(target string, isIngress bool) (res *ConnectivityResult, ipb *common.IPBlock) { diff --git a/pkg/ibmvpc/vpc.go b/pkg/ibmvpc/vpc.go index bed915a77..1cabf8b79 100644 --- a/pkg/ibmvpc/vpc.go +++ b/pkg/ibmvpc/vpc.go @@ -275,7 +275,7 @@ func (nl *NaclLayer) AllowedConnectivity(src, dst vpcmodel.Node, isIngress bool) } // RulesInConnectivity list of SG rules contributing to the connectivity -func (nl *NaclLayer) RulesInConnectivity(vpcmodel.Node, vpcmodel.Node, bool) ([]vpcmodel.RulesInFilter, error) { +func (nl *NaclLayer) RulesInConnectivity(vpcmodel.Node, vpcmodel.Node, *common.ConnectionSet, bool) ([]vpcmodel.RulesInFilter, error) { return nil, nil } @@ -378,12 +378,15 @@ func (sgl *SecurityGroupLayer) AllowedConnectivity(src, dst vpcmodel.Node, isIng } func (sgl *SecurityGroupLayer) RulesInConnectivity(src, dst vpcmodel.Node, - isIngress bool) (res []vpcmodel.RulesInFilter, err error) { + conn *common.ConnectionSet, isIngress bool) (res []vpcmodel.RulesInFilter, err error) { if connHasIKSNode(src, dst, isIngress) { return nil, fmt.Errorf("explainability for IKS node not supported yet") } for indx, sg := range sgl.sgList { - sgRules := sg.RulesInConnectivity(src, dst, isIngress) + sgRules, err1 := sg.RulesInConnectivity(src, dst, conn, isIngress) + if err1 != nil { + return nil, err1 + } if len(sgRules) > 0 { rulesInSg := vpcmodel.RulesInFilter{ Filter: indx, @@ -432,12 +435,12 @@ func (sg *SecurityGroup) AllowedConnectivity(src, dst vpcmodel.Node, isIngress b } // RulesInConnectivity list of SG rules contributing to the connectivity -func (sg *SecurityGroup) RulesInConnectivity(src, dst vpcmodel.Node, isIngress bool) []int { +func (sg *SecurityGroup) RulesInConnectivity(src, dst vpcmodel.Node, conn *common.ConnectionSet, isIngress bool) ([]int, error) { memberStrAddress, targetStrAddress := sg.getMemberTargetStrAddress(src, dst, isIngress) if _, ok := sg.members[memberStrAddress]; !ok { - return nil // connectivity not affected by this SG resource - input node is not its member + return nil, nil // connectivity not affected by this SG resource - input node is not its member } - return sg.analyzer.rulesInConnectivity(targetStrAddress, isIngress) + return sg.analyzer.rulesInConnectivity(targetStrAddress, conn, isIngress) } func (sg *SecurityGroup) getMemberTargetStrAddress(src, dst vpcmodel.Node, diff --git a/pkg/vpcmodel/abstractVPC.go b/pkg/vpcmodel/abstractVPC.go index db4fba221..76019b1cb 100644 --- a/pkg/vpcmodel/abstractVPC.go +++ b/pkg/vpcmodel/abstractVPC.go @@ -88,7 +88,7 @@ type FilterTrafficResource interface { AllowedConnectivity(src, dst Node, isIngress bool) (*common.ConnectionSet, error) // RulesInConnectivity get the list of rules of a given filter that contributes to the connection between src and dst // todo: currently implemented only to sg; likely src and dst will be VPCResourceIntf instead of Node - RulesInConnectivity(src, dst Node, isIngress bool) ([]RulesInFilter, error) + RulesInConnectivity(src, dst Node, conn *common.ConnectionSet, isIngress bool) ([]RulesInFilter, error) StringRulesOfFilter(listRulesInFilter []RulesInFilter) string ReferencedIPblocks() []*common.IPBlock ConnectivityMap() (map[string]*IPbasedConnectivityResult, error) diff --git a/pkg/vpcmodel/nodesExplainability.go b/pkg/vpcmodel/nodesExplainability.go index 15b0f16c8..6348b3613 100644 --- a/pkg/vpcmodel/nodesExplainability.go +++ b/pkg/vpcmodel/nodesExplainability.go @@ -28,6 +28,7 @@ type explainStruct []*rulesSingleSrcDst type explanation struct { c *VPCConfig + connQuery *common.ConnectionSet explainStruct *explainStruct // grouped connectivity result: // grouping common explanation lines with common src/dst (internal node) and different dst/src (external node) @@ -99,26 +100,30 @@ func (c *VPCConfig) getNodesFromInput(cidrOrName string) ([]Node, error) { // 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) +// todo: connection should be given in a string format, of course +// nil conn means connection is not part of the query +func (c *VPCConfig) ExplainConnectivity(src, dst string, connQuery *common.ConnectionSet) (out string, err error) { + explanationStruct, err1 := c.computeExplainRules(src, dst, connQuery) if err1 != nil { return "", err1 } - err2 := explanationStruct.computeConnections(c) - if err2 != nil { - return "", err2 + if connQuery == nil { // find the connection between src and dst if connection not specified in query + 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} + res := &explanation{c, connQuery, &explanationStruct, groupedLines.GroupedLines} return res.String(), nil } // computeExplainRules computes the egress and ingress rules contributing to the (existing or missing) connection -func (c *VPCConfig) computeExplainRules(srcName, dstName string) (explanationStruct explainStruct, err error) { - srcNodes, dstNodes, err := c.processInput(srcName, dstName) +func (c *VPCConfig) computeExplainRules(srcName, dstName string, conn *common.ConnectionSet) (explanationStruct explainStruct, err error) { + srcNodes, dstNodes, err := c.processInput(srcName, dstName) // todo: should also handle connection string translation if err != nil { return nil, err } @@ -128,7 +133,7 @@ func (c *VPCConfig) computeExplainRules(srcName, dstName string) (explanationStr // the loop is on two dimension since we do not know which, but actually we have a single dimension for _, src := range srcNodes { for _, dst := range dstNodes { - rulesOfConnection, err := c.getRulesOfConnection(src, dst) + rulesOfConnection, err := c.getRulesOfConnection(src, dst, conn) if err != nil { return nil, err } @@ -163,26 +168,26 @@ func (c *VPCConfig) processInput(srcName, dstName string) (srcNodes, dstNodes [] } func (c *VPCConfig) getFiltersEnablingRulesBetweenNodesPerDirectionAndLayer( - src, dst Node, isIngress bool, layer string) (rules *[]RulesInFilter, err error) { + src, dst Node, conn *common.ConnectionSet, isIngress bool, layer string) (rules *[]RulesInFilter, err error) { filter := c.getFilterTrafficResourceOfKind(layer) if filter == nil { return nil, fmt.Errorf("layer %v not found in configuration", layer) } - rulesOfFilter, err := filter.RulesInConnectivity(src, dst, isIngress) + rulesOfFilter, err := filter.RulesInConnectivity(src, dst, conn, isIngress) if err != nil { return nil, err } return &rulesOfFilter, nil } -func (c *VPCConfig) getRulesOfConnection(src, dst Node) (rulesOfConnection *rulesConnection, err error) { +func (c *VPCConfig) getRulesOfConnection(src, dst Node, conn *common.ConnectionSet) (rulesOfConnection *rulesConnection, err error) { filterLayers := []string{SecurityGroupLayer} rulesOfConnection = &rulesConnection{} ingressRulesPerLayer, egressRulesPerLayer := make(rulesInLayers), make(rulesInLayers) for _, layer := range filterLayers { // ingress rules: relevant only if dst is internal if dst.IsInternal() { - ingressRules, err1 := c.getFiltersEnablingRulesBetweenNodesPerDirectionAndLayer(src, dst, true, layer) + ingressRules, err1 := c.getFiltersEnablingRulesBetweenNodesPerDirectionAndLayer(src, dst, conn, true, layer) if err1 != nil { return nil, err1 } @@ -193,7 +198,7 @@ func (c *VPCConfig) getRulesOfConnection(src, dst Node) (rulesOfConnection *rule // egress rules: relevant only is src is internal if src.IsInternal() { - egressRules, err2 := c.getFiltersEnablingRulesBetweenNodesPerDirectionAndLayer(src, dst, false, layer) + egressRules, err2 := c.getFiltersEnablingRulesBetweenNodesPerDirectionAndLayer(src, dst, conn, false, layer) if err2 != nil { return nil, err2 } @@ -229,10 +234,10 @@ func (c *VPCConfig) getContainingConfigNode(node Node) (Node, error) { } // prints each separately without grouping - for debug -func (explanationStruct *explainStruct) String(c *VPCConfig) (string, error) { +func (explanationStruct *explainStruct) String(c *VPCConfig, connQuery *common.ConnectionSet) (string, error) { resStr := "" for _, rulesSrcDst := range *explanationStruct { - resStr += stringExplainabilityLine(c, rulesSrcDst.src, rulesSrcDst.dst, rulesSrcDst.conn, rulesSrcDst.rules) + resStr += stringExplainabilityLine(c, connQuery, rulesSrcDst.src, rulesSrcDst.dst, rulesSrcDst.conn, rulesSrcDst.rules) } return resStr, nil } @@ -241,21 +246,27 @@ 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, + linesStr[i] = stringExplainabilityLine(explanation.c, explanation.connQuery, line.src, line.dst, line.commonProperties.conn, line.commonProperties.rules) } sort.Strings(linesStr) return strings.Join(linesStr, "\n") + "\n" } -func stringExplainabilityLine(c *VPCConfig, src, dst EndpointElem, conn *common.ConnectionSet, rules *rulesConnection) string { +func stringExplainabilityLine(c *VPCConfig, connQuery *common.ConnectionSet, 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()) + noConnection := "" + if connQuery == nil { + noConnection = fmt.Sprintf("No connection between %v and %v;", src.Name(), dst.Name()) + } else { + noConnection = fmt.Sprintf("There is no connection \"%v\" between %v and %v;", connQuery.String(), src.Name(), dst.Name()) + } resStr := "" switch { case noIngressRules && noEgressRules: @@ -271,8 +282,13 @@ func stringExplainabilityLine(c *VPCConfig, src, dst EndpointElem, conn *common. resStr += ingressRulesStr } default: // there is a connection - resStr = fmt.Sprintf("The following connection exists between %v and %v: %v; its enabled by\n", src.Name(), dst.Name(), - conn.String()) + if connQuery == nil { + resStr = fmt.Sprintf("The following connection exists between %v and %v: %v; its enabled by\n", src.Name(), dst.Name(), + conn.String()) + } else { + resStr = fmt.Sprintf("Connection %v exists between %v and %v; its enabled by\n", connQuery.String(), + src.Name(), dst.Name()) + } if needEgress { resStr += egressRulesStr }