diff --git a/cmd/analyzer/main.go b/cmd/analyzer/main.go index 5f13ce761..ea8696c1b 100644 --- a/cmd/analyzer/main.go +++ b/cmd/analyzer/main.go @@ -6,7 +6,6 @@ import ( "log" "os" - "github.com/np-guard/vpc-network-config-analyzer/pkg/common" "github.com/np-guard/vpc-network-config-analyzer/pkg/ibmvpc" "github.com/np-guard/vpc-network-config-analyzer/pkg/version" "github.com/np-guard/vpc-network-config-analyzer/pkg/vpcmodel" @@ -56,10 +55,17 @@ func analysisTypeToUseCase(inArgs *InArgs) vpcmodel.OutputUseCase { } func analysisVPCConfigs(c1, c2 map[string]*vpcmodel.VPCConfig, inArgs *InArgs, outFile string) (string, error) { + var explanationArgs *vpcmodel.ExplanationArgs + if *inArgs.AnalysisType == explainMode { + explanationArgs = vpcmodel.NewExplanationArgs(*inArgs.ESrc, *inArgs.EDst, *inArgs.EProtocol, + *inArgs.ESrcMinPort, *inArgs.ESrcMaxPort, *inArgs.EDstMinPort, *inArgs.EDstMaxPort) + } + og, err := vpcmodel.NewOutputGenerator(c1, c2, *inArgs.Grouping, analysisTypeToUseCase(inArgs), - false) + false, + explanationArgs) if err != nil { return "", err } @@ -86,19 +92,6 @@ func vpcConfigsFromFile(fileName string, inArgs *InArgs) (map[string]*vpcmodel.V return vpcConfigs, nil } -func translateCDtoConnectionSet(inArgs *InArgs) *common.ConnectionSet { - connection := common.NewConnectionSet(false) - if common.ProtocolStr(*inArgs.QProtocol) == common.ProtocolICMP { - connection.AddICMPConnection(common.MinICMPtype, common.MaxICMPtype, - common.MinICMPcode, common.MaxICMPcode) - } else { - connection.AddTCPorUDPConn(common.ProtocolStr(*inArgs.QProtocol), *inArgs.QSrcMinPort, *inArgs.QSrcMaxPort, - *inArgs.QDstMinPort, *inArgs.QDstMaxPort) - } - - return connection -} - // The actual main function // Takes command-line flags and returns an error rather than exiting, so it can be more easily used in testing func _main(cmdlineArgs []string) error { @@ -140,10 +133,6 @@ func _main(cmdlineArgs []string) error { } fmt.Println(vpcAnalysisOutput) - if *inArgs.AnalysisType == explainMode { - _ = translateCDtoConnectionSet(inArgs) - } - return nil } diff --git a/cmd/analyzer/parse_args.go b/cmd/analyzer/parse_args.go index fbc1f7c05..8447791b0 100644 --- a/cmd/analyzer/parse_args.go +++ b/cmd/analyzer/parse_args.go @@ -20,11 +20,13 @@ type InArgs struct { VPC *string Debug *bool Version *bool - QProtocol *string - QSrcMinPort *int64 - QSrcMaxPort *int64 - QDstMinPort *int64 - QDstMaxPort *int64 + ESrc *string + EDst *string + EProtocol *string + ESrcMinPort *int64 + ESrcMaxPort *int64 + EDstMinPort *int64 + EDstMaxPort *int64 } // flagHasValue indicates for each input arg if it is expected to have a value in the cli or not @@ -38,11 +40,13 @@ var flagHasValue = map[string]bool{ VPC: true, Debug: false, Version: false, - QProtocol: true, - QSrcMinPort: true, - QSrcMaxPort: true, - QDstMinPort: true, - QDstMaxPort: true, + ESrc: true, + EDst: true, + EProtocol: true, + ESrcMinPort: true, + ESrcMaxPort: true, + EDstMinPort: true, + EDstMaxPort: true, } const ( @@ -56,11 +60,13 @@ const ( VPC = "vpc" Debug = "debug" Version = "version" - QProtocol = "q-protocol" - QSrcMinPort = "q-src-min-port" - QSrcMaxPort = "q-src-max-port" - QDstMinPort = "q-dst-min-port" - QDstMaxPort = "q-dst-max-port" + ESrc = "src" + EDst = "dst" + EProtocol = "protocol" + ESrcMinPort = "src-min-port" + ESrcMaxPort = "src-max-port" + EDstMinPort = "dst-min-port" + EDstMaxPort = "dst-max-port" // output formats supported JSONFormat = "json" @@ -180,11 +186,13 @@ func ParseInArgs(cmdlineArgs []string) (*InArgs, error) { args.VPC = flagset.String(VPC, "", "CRN of the VPC to analyze") args.Debug = flagset.Bool(Debug, false, "Run in debug mode") args.Version = flagset.Bool(Version, false, "Prints the release version number") - args.QProtocol = flagset.String(QProtocol, "", "Protocol for connection description") - args.QSrcMinPort = flagset.Int64(QSrcMinPort, common.MinPort, "SrcMinPort for connection description") - args.QSrcMaxPort = flagset.Int64(QSrcMaxPort, common.MaxPort, "SrcMaxPort for connection description") - args.QDstMinPort = flagset.Int64(QDstMinPort, common.MinPort, "DstMinPort for connection description") - args.QDstMaxPort = flagset.Int64(QDstMaxPort, common.MaxPort, "DstMaxPort for connection description") + args.ESrc = flagset.String(ESrc, "", "Src name for network_interface or an external ip to be explained") + args.EDst = flagset.String(EDst, "", "Dst name for network_interface or an external ip to be explained") + args.EProtocol = flagset.String(EProtocol, "", "Protocol for connection description") + args.ESrcMinPort = flagset.Int64(ESrcMinPort, common.MinPort, "minimum source port for connection description") + args.ESrcMaxPort = flagset.Int64(ESrcMaxPort, common.MaxPort, "maximum source port for connection description") + args.EDstMinPort = flagset.Int64(EDstMinPort, common.MinPort, "minimum destination port for connection description") + args.EDstMaxPort = flagset.Int64(EDstMaxPort, common.MaxPort, "maximum destination port for connection description") // calling parseCmdLine prior to flagset.Parse to ensure that excessive and unsupported arguments are handled // for example, flagset.Parse() ignores input args missing the `-` @@ -223,13 +231,15 @@ func wasFlagSpecified(name string, flagset *flag.FlagSet) bool { return found } -func wereExplainParamsSpecified(flagset *flag.FlagSet) bool { - if wasFlagSpecified(QProtocol, flagset) || wasFlagSpecified(QSrcMinPort, flagset) || wasFlagSpecified(QSrcMaxPort, flagset) || - wasFlagSpecified(QDstMinPort, flagset) || wasFlagSpecified(QDstMaxPort, flagset) { - return true +func wereExplainParamsSpecified(flagset *flag.FlagSet, flagNames []string) bool { + specified := false + for i := 0; i < len(flagNames); i++ { + if wasFlagSpecified(flagNames[i], flagset) { + specified = true + } } - return false + return specified } func PortInRange(port int64) bool { @@ -249,39 +259,50 @@ func minMaxValidity(minPort, maxPort int64, minPortName, maxPortName string) err } func validRangeConnectionExplainMode(args *InArgs) error { - err := minMaxValidity(*args.QSrcMinPort, *args.QSrcMaxPort, QSrcMinPort, QSrcMaxPort) + err := minMaxValidity(*args.ESrcMinPort, *args.ESrcMaxPort, ESrcMinPort, ESrcMaxPort) if err != nil { return err } - err = minMaxValidity(*args.QDstMinPort, *args.QDstMaxPort, QDstMinPort, QDstMaxPort) + err = minMaxValidity(*args.EDstMinPort, *args.EDstMaxPort, EDstMinPort, EDstMaxPort) if err != nil { return err } - if !PortInRange(*args.QSrcMinPort) || !PortInRange(*args.QSrcMaxPort) || - !PortInRange(*args.QDstMinPort) || !PortInRange(*args.QDstMaxPort) { + if !PortInRange(*args.ESrcMinPort) || !PortInRange(*args.ESrcMaxPort) || + !PortInRange(*args.EDstMinPort) || !PortInRange(*args.EDstMaxPort) { return fmt.Errorf("%s, %s, %s and %s must be in ranges [%d, %d]", - QSrcMinPort, QSrcMaxPort, QDstMinPort, QDstMaxPort, common.MinPort, common.MaxPort) + ESrcMinPort, ESrcMaxPort, EDstMinPort, EDstMaxPort, common.MinPort, common.MaxPort) } return nil } func invalidArgsExplainMode(args *InArgs, flagset *flag.FlagSet) error { - if *args.AnalysisType != explainMode && wereExplainParamsSpecified(flagset) { - return fmt.Errorf("%s, %s, %s, %s and %s can be specified only when analysis-type is %s", - QProtocol, QSrcMinPort, QSrcMaxPort, QDstMinPort, QDstMaxPort, explainMode) + if *args.AnalysisType != explainMode { + if wereExplainParamsSpecified(flagset, []string{ESrc, EDst, EProtocol, ESrcMinPort, ESrcMaxPort, EDstMinPort, EDstMaxPort, explainMode}) { + return fmt.Errorf("explainability related params %s, %s, %s, %s, %s, %s and %s"+ + "can be specified only in explain mode: analysis-type equals %s", + ESrc, EDst, EProtocol, ESrcMinPort, ESrcMaxPort, EDstMinPort, EDstMaxPort, explainMode) + } + return nil } - if *args.AnalysisType != explainMode { + if *args.ESrc == "" || *args.EDst == "" { + return fmt.Errorf("please specify %s and %s network_interface / external ip you want to explain connectivity for", ESrc, EDst) + } + + if *args.EProtocol == "" { + if wereExplainParamsSpecified(flagset, []string{EProtocol, ESrcMinPort, ESrcMaxPort, EDstMinPort, EDstMaxPort, explainMode}) { + return fmt.Errorf("protocol must be specified when querying a specific connection") + } return nil } - protocol := strings.ToUpper(*args.QProtocol) + protocol := strings.ToUpper(*args.EProtocol) if protocol != string(common.ProtocolTCP) && protocol != string(common.ProtocolUDP) && protocol != string(common.ProtocolICMP) { return fmt.Errorf("wrong connection description protocol '%s'; must be one of: 'TCP, UDP, ICMP'", protocol) } - args.QProtocol = &protocol + args.EProtocol = &protocol return validRangeConnectionExplainMode(args) } diff --git a/pkg/ibmvpc/analysis_output_test.go b/pkg/ibmvpc/analysis_output_test.go index 3d249f154..572b08c06 100644 --- a/pkg/ibmvpc/analysis_output_test.go +++ b/pkg/ibmvpc/analysis_output_test.go @@ -41,6 +41,13 @@ type vpcGeneralTest struct { grouping bool format vpcmodel.OutFormat vpc string + ESrc string + EDst string + EProtocol string + ESrcMinPort int64 + ESrcMaxPort int64 + EDstMinPort int64 + EDstMaxPort int64 } const ( @@ -52,6 +59,7 @@ const ( suffixOutFileSubnetsLevelNoPGW = "subnetsBased_withoutPGW" suffixOutFileDiffSubnets = "subnetsDiff" suffixOutFileDiffEndpoints = "endpointsDiff" + suffixOutFileExplain = "explain" txtOutSuffix = ".txt" debugOutSuffix = "_debug.txt" mdOutSuffix = ".md" @@ -93,6 +101,8 @@ func getTestFileName(testName string, res = baseName + suffixOutFileDiffSubnets case vpcmodel.EndpointsDiff: res = baseName + suffixOutFileDiffEndpoints + case vpcmodel.Explain: + res = baseName + suffixOutFileExplain } if grouping { res += suffixOutFileWithGrouping @@ -184,6 +194,23 @@ var tests = []*vpcGeneralTest{ useCases: []vpcmodel.OutputUseCase{vpcmodel.AllEndpoints, vpcmodel.SingleSubnet}, format: vpcmodel.Text, }, + { + name: "acl_testing3", + useCases: []vpcmodel.OutputUseCase{vpcmodel.Explain}, + format: vpcmodel.Text, + ESrc: "vsi1-ky[10.240.10.4]", + EDst: "vsi2-ky[10.240.20.4]", + }, + { + name: "sg_testing1_new", + useCases: []vpcmodel.OutputUseCase{vpcmodel.Explain}, + format: vpcmodel.Text, + ESrc: "vsi1-ky[10.240.10.4]", + EDst: "vsi2-ky[10.240.20.4]", + EProtocol: "TCP", + ESrcMinPort: 1, + ESrcMaxPort: 5, + }, { name: "sg_testing1_new", useCases: []vpcmodel.OutputUseCase{vpcmodel.AllEndpoints, vpcmodel.SingleSubnet, vpcmodel.AllSubnets}, @@ -493,10 +520,14 @@ func (tt *vpcGeneralTest) runTest(t *testing.T) { vpcConfigs := getVPCConfigs(t, tt, true) var vpcConfigs2nd map[string]*vpcmodel.VPCConfig diffUseCase := false + explainUseCase := false for _, useCase := range tt.useCases { if useCase == vpcmodel.SubnetsDiff || useCase == vpcmodel.EndpointsDiff { diffUseCase = true } + if useCase == vpcmodel.Explain { + explainUseCase = true + } } if diffUseCase { vpcConfigs2nd = getVPCConfigs(t, tt, false) @@ -504,9 +535,15 @@ func (tt *vpcGeneralTest) runTest(t *testing.T) { tt.inputConfig2nd = "" } + var explanationArgs *vpcmodel.ExplanationArgs + if explainUseCase { + explanationArgs = vpcmodel.NewExplanationArgs(tt.ESrc, tt.EDst, tt.EProtocol, + tt.ESrcMinPort, tt.ESrcMaxPort, tt.EDstMinPort, tt.EDstMaxPort) + } + // generate actual output for all use cases specified for this test for _, uc := range tt.useCases { - err := runTestPerUseCase(t, tt, vpcConfigs, vpcConfigs2nd, uc, tt.mode) + err := runTestPerUseCase(t, tt, vpcConfigs, vpcConfigs2nd, uc, tt.mode, explanationArgs) require.Equal(t, tt.errPerUseCase[uc], err, "comparing actual err to expected err") } for uc, outFile := range tt.actualOutput { @@ -579,11 +616,12 @@ func runTestPerUseCase(t *testing.T, tt *vpcGeneralTest, c1, c2 map[string]*vpcmodel.VPCConfig, uc vpcmodel.OutputUseCase, - mode testMode) error { + mode testMode, + explanationArgs *vpcmodel.ExplanationArgs) error { if err := initTestFileNames(tt, uc, "", true); err != nil { return err } - og, err := vpcmodel.NewOutputGenerator(c1, c2, tt.grouping, uc, tt.format == vpcmodel.ARCHDRAWIO) + og, err := vpcmodel.NewOutputGenerator(c1, c2, tt.grouping, uc, tt.format == vpcmodel.ARCHDRAWIO, explanationArgs) if err != nil { return err } diff --git a/pkg/ibmvpc/examples/acl_testing3_all_vpcs_explain.txt b/pkg/ibmvpc/examples/acl_testing3_all_vpcs_explain.txt new file mode 100644 index 000000000..c74326238 --- /dev/null +++ b/pkg/ibmvpc/examples/acl_testing3_all_vpcs_explain.txt @@ -0,0 +1,15 @@ +Connectivity explanation between vsi1-ky[10.240.10.4] and vsi2-ky[10.240.20.4] +The following connection exists between vsi1-ky[10.240.10.4] and vsi2-ky[10.240.20.4]: protocol: TCP,UDP; its enabled by +Egress Rules: +~~~~~~~~~~~~~ +SecurityGroupLayer Rules +------------------------ +enabling rules from sg1-ky: + index: 0, direction: outbound, protocol: all, cidr: 0.0.0.0/0 +Ingress Rules: +~~~~~~~~~~~~~~ +SecurityGroupLayer Rules +------------------------ +enabling rules from sg1-ky: + index: 1, direction: inbound, protocol: all, cidr: 0.0.0.0/0 + diff --git a/pkg/ibmvpc/examples/sg_testing1_new_all_vpcs_explain.txt b/pkg/ibmvpc/examples/sg_testing1_new_all_vpcs_explain.txt new file mode 100644 index 000000000..c8a1e118b --- /dev/null +++ b/pkg/ibmvpc/examples/sg_testing1_new_all_vpcs_explain.txt @@ -0,0 +1,9 @@ +Connectivity explanation between vsi1-ky[10.240.10.4] and vsi2-ky[10.240.20.4] +There is no connection "protocol: TCP src-ports: 1-5 dst-ports: 0" between vsi1-ky[10.240.10.4] and vsi2-ky[10.240.20.4]; connection blocked by egress +Ingress Rules: +~~~~~~~~~~~~~~ +SecurityGroupLayer Rules +------------------------ +enabling rules from sg2-ky: + index: 4, direction: inbound, protocol: all, cidr: 10.240.10.4/32 + diff --git a/pkg/ibmvpc/explainability_test.go b/pkg/ibmvpc/explainability_test.go index 5a690ae56..b2995f7e0 100644 --- a/pkg/ibmvpc/explainability_test.go +++ b/pkg/ibmvpc/explainability_test.go @@ -34,10 +34,11 @@ func TestVsiToVsi(t *testing.T) { if vpcConfig == nil { require.Fail(t, "vpcConfig equals nil") } - explainStr1, err1 := vpcConfig.ExplainConnectivity("vsi2-ky[10.240.20.4]", "vsi3b-ky[10.240.30.4]", nil) + explain1, err1 := vpcConfig.ExplainConnectivity("vsi2-ky[10.240.20.4]", "vsi3b-ky[10.240.30.4]", nil) if err1 != nil { require.Fail(t, err1.Error()) } + explainStr1 := explain1.String() fmt.Println(explainStr1) require.Equal(t, "The following connection exists between vsi2-ky[10.240.20.4] and vsi3b-ky[10.240.30.4]: "+ "protocol: TCP; its enabled by\nEgress Rules:\n~~~~~~~~~~~~~\n"+ @@ -47,10 +48,11 @@ 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", explainStr1) - explainStr2, err2 := vpcConfig.ExplainConnectivity("vsi2-ky[10.240.20.4]", "vsi1-ky[10.240.10.4]", nil) + explain2, err2 := vpcConfig.ExplainConnectivity("vsi2-ky[10.240.20.4]", "vsi1-ky[10.240.10.4]", nil) if err2 != nil { require.Fail(t, err2.Error()) } + explainStr2 := explain2.String() fmt.Println(explainStr2) 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"+ @@ -58,10 +60,11 @@ 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", explainStr2) - explainStr3, err3 := vpcConfig.ExplainConnectivity("vsi3a-ky[10.240.30.5]", "vsi1-ky[10.240.10.4]", nil) + explain3, err3 := vpcConfig.ExplainConnectivity("vsi3a-ky[10.240.30.5]", "vsi1-ky[10.240.10.4]", nil) if err3 != nil { require.Fail(t, err3.Error()) } + explainStr3 := explain3.String() fmt.Println(explainStr3) 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"+ @@ -70,18 +73,20 @@ func TestVsiToVsi(t *testing.T) { "\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", explainStr3) - explainStr4, err4 := vpcConfig.ExplainConnectivity("vsi1-ky[10.240.10.4]", "vsi2-ky[10.240.20.4]", nil) + explain4, err4 := vpcConfig.ExplainConnectivity("vsi1-ky[10.240.10.4]", "vsi2-ky[10.240.20.4]", nil) if err4 != nil { require.Fail(t, err4.Error()) } + explainStr4 := explain4.String() fmt.Println(explainStr4) 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", explainStr4) - explainStr5, err5 := vpcConfig.ExplainConnectivity("vsi3a-ky[10.240.30.5]", "vsi2-ky[10.240.20.4]", nil) + explain5, err5 := vpcConfig.ExplainConnectivity("vsi3a-ky[10.240.30.5]", "vsi2-ky[10.240.20.4]", nil) if err5 != nil { require.Fail(t, err5.Error()) } + explainStr5 := explain5.String() fmt.Println(explainStr5) 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"+ @@ -118,20 +123,22 @@ func TestSimpleExternalSG(t *testing.T) { } vsi1 := "vsi1-ky[10.240.10.4]" cidr1 := "161.26.0.0/16" - explainStr1, err1 := vpcConfig.ExplainConnectivity(vsi1, cidr1, nil) + explain1, err1 := vpcConfig.ExplainConnectivity(vsi1, cidr1, nil) if err1 != nil { require.Fail(t, err1.Error()) } + explainStr1 := explain1.String() fmt.Println(explainStr1) fmt.Println("---------------------------------------------------------------------------------------------------------------------------") 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\nExternal Router PublicGateway: public-gw-ky\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", explainStr1) - explainStr2, err2 := vpcConfig.ExplainConnectivity(cidr1, vsi1, nil) + explain2, err2 := vpcConfig.ExplainConnectivity(cidr1, vsi1, nil) if err2 != nil { require.Fail(t, err2.Error()) } + explainStr2 := explain2.String() fmt.Println(explainStr2) fmt.Println("-------------------------------------------------------------------------------------" + "--------------------------------------") @@ -139,10 +146,11 @@ func TestSimpleExternalSG(t *testing.T) { "no fip router and src is external (fip is required for outbound external connection)\n\n", explainStr2) cidr2 := "161.26.0.0/32" - explainStr3, err3 := vpcConfig.ExplainConnectivity(vsi1, cidr2, nil) + explain3, err3 := vpcConfig.ExplainConnectivity(vsi1, cidr2, nil) if err3 != nil { require.Fail(t, err3.Error()) } + explainStr3 := explain3.String() fmt.Println(explainStr3) fmt.Println("---------------------------------------------------------------------------------------------------------------------------") require.Equal(t, "The following connection exists between vsi1-ky[10.240.10.4] and Public Internet 161.26.0.0/32: "+ @@ -150,10 +158,11 @@ func TestSimpleExternalSG(t *testing.T) { "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", explainStr3) vsi3b := "vsi3b-ky[10.240.30.4]" - explainStr4, err4 := vpcConfig.ExplainConnectivity(vsi3b, cidr2, nil) + explain4, err4 := vpcConfig.ExplainConnectivity(vsi3b, cidr2, nil) if err4 != nil { require.Fail(t, err4.Error()) } + explainStr4 := explain4.String() fmt.Println(explainStr4) fmt.Println("--------------------------------------------------------------------------------------------------" + "-------------------------") @@ -169,10 +178,11 @@ func TestGroupingExternalSG(t *testing.T) { } vsi1 := "vsi1-ky[10.240.10.4]" cidr1 := "161.26.0.0/8" - explainStr1, err1 := vpcConfig.ExplainConnectivity(vsi1, cidr1, nil) + explain1, err1 := vpcConfig.ExplainConnectivity(vsi1, cidr1, nil) if err1 != nil { require.Fail(t, err1.Error()) } + explainStr1 := explain1.String() fmt.Println(explainStr1) fmt.Println("---------------------------------------------------------------------------------------------------------------------------") 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; "+ @@ -185,10 +195,11 @@ func TestGroupingExternalSG(t *testing.T) { // todo: now that external and internal IPs are treated differently, deffer cidrAll test to the time we properly support internal IP #305 // vsi2 := "vsi2-ky[10.240.20.4]" // cidrAll := "0.0.0.0/0" - // explainStr2, err2 := vpcConfig.ExplainConnectivity(vsi2, cidrAll, nil) + // explain2, err2 := vpcConfig.ExplainConnectivity(vsi2, cidrAll, nil) // if err2 != nil { // require.Fail(t, err2.Error()) // } + // explainStr2 := explain2.String() // fmt.Println(explainStr2) // fmt.Println("-------------------------------------------------------------------------- // -------------------------------------------------") @@ -198,10 +209,11 @@ func TestGroupingExternalSG(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", // explainStr2) - // explainStr3, err3 := vpcConfig.ExplainConnectivity(cidrAll, vsi2, nil) + // explain3, err3 := vpcConfig.ExplainConnectivity(cidrAll, vsi2, nil) // if err3 != nil { // require.Fail(t, err3.Error()) // } + // explainStr3 := explain3.String() // fmt.Println(explainStr3) // 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] "+ @@ -219,10 +231,11 @@ func TestQueryConnectionSGBasic(t *testing.T) { require.Fail(t, "vpcConfig equals nil") } // test1: a connection exists, but it is not the required one by query - explainStr1, err1 := vpcConfig.ExplainConnectivity("vsi2-ky[10.240.20.4]", "vsi3b-ky[10.240.30.4]", common.NewConnectionSet(true)) + explain1, 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()) } + explainStr1 := explain1.String() fmt.Println(explainStr1) fmt.Println("---------------------------------------------------------------------------------------------------------------------------") require.Equal(t, "There is no connection \"All Connections\" between vsi2-ky[10.240.20.4] and vsi3b-ky[10.240.30.4]; "+ @@ -235,10 +248,11 @@ func TestQueryConnectionSGBasic(t *testing.T) { cidr1 := "161.26.0.0/16" connectionUDP1 := common.NewConnectionSet(false) connectionUDP1.AddTCPorUDPConn(common.ProtocolUDP, common.MinPort, common.MaxPort, common.MinPort, common.MaxPort) - explainStr2, err2 := vpcConfig.ExplainConnectivity(vsi1, cidr1, connectionUDP1) + explain2, err2 := vpcConfig.ExplainConnectivity(vsi1, cidr1, connectionUDP1) if err2 != nil { require.Fail(t, err2.Error()) } + explainStr2 := explain2.String() fmt.Println(explainStr2) fmt.Println("---------------------------------------------------------------------------------------------------------------------------") 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"+ @@ -250,10 +264,11 @@ func TestQueryConnectionSGBasic(t *testing.T) { //test3: the required connection is contained in the existing one per connection connectionUDP2 := common.NewConnectionSet(false) connectionUDP2.AddTCPorUDPConn(common.ProtocolUDP, 10, 100, 443, 443) - explainStr3, err3 := vpcConfig.ExplainConnectivity(vsi1, cidr1, connectionUDP2) + explain3, err3 := vpcConfig.ExplainConnectivity(vsi1, cidr1, connectionUDP2) if err3 != nil { require.Fail(t, err3.Error()) } + explainStr3 := explain3.String() fmt.Println(explainStr3) fmt.Println("---------------------------------------------------------------------------------------------------------------------------") require.Equal(t, "Connection protocol: UDP src-ports: 10-100 dst-ports: 443 exists between vsi1-ky[10.240.10.4] "+ @@ -264,10 +279,11 @@ func TestQueryConnectionSGBasic(t *testing.T) { // test4: the required connection is contained in the existing one per ip of src/dst cidr2 := "161.26.0.0/20" - explainStr4, err4 := vpcConfig.ExplainConnectivity(vsi1, cidr2, connectionUDP2) + explain4, err4 := vpcConfig.ExplainConnectivity(vsi1, cidr2, connectionUDP2) if err4 != nil { require.Fail(t, err4.Error()) } + explainStr4 := explain4.String() fmt.Println(explainStr4) fmt.Println("---------------------------------------------------------------------------------------------------------------------------") require.Equal(t, "Connection protocol: UDP src-ports: 10-100 dst-ports: 443 exists between vsi1-ky[10.240.10.4] "+ @@ -278,10 +294,11 @@ func TestQueryConnectionSGBasic(t *testing.T) { // test5: the required connection exists for part of the dst ip cidr3 := "161.26.0.0/12" - explainStr5, err5 := vpcConfig.ExplainConnectivity(vsi1, cidr3, connectionUDP2) + explain5, err5 := vpcConfig.ExplainConnectivity(vsi1, cidr3, connectionUDP2) if err5 != nil { require.Fail(t, err5.Error()) } + explainStr5 := explain5.String() fmt.Println(explainStr5) fmt.Println("-------------------------------------------------------------------------------------" + "--------------------------------------") @@ -295,10 +312,11 @@ func TestQueryConnectionSGBasic(t *testing.T) { "connection blocked by egress\n\n", explainStr5) // test6: a connection does not exist regardless of the query - explainStr6, err6 := vpcConfig.ExplainConnectivity("vsi1-ky[10.240.10.4]", "vsi3a-ky[10.240.30.5]", connectionUDP2) + explain6, err6 := vpcConfig.ExplainConnectivity("vsi1-ky[10.240.10.4]", "vsi3a-ky[10.240.30.5]", connectionUDP2) if err6 != nil { require.Fail(t, err6.Error()) } + explainStr6 := explain6.String() fmt.Println(explainStr6) fmt.Println("---------------------------------------------------------------------------------------------------------------------------") require.Equal(t, "There is no connection \"protocol: UDP src-ports: 10-100 dst-ports: 443\" "+ @@ -314,10 +332,11 @@ func TestQueryConnectionSGRules(t *testing.T) { // test1: all rules are relevant (for comparison) vsi1 := "vsi1-ky[10.240.10.4]" vsi3a := "vsi3a-ky[10.240.30.5]" - explainStr1, err1 := vpcConfig.ExplainConnectivity(vsi3a, vsi1, nil) + explain1, err1 := vpcConfig.ExplainConnectivity(vsi3a, vsi1, nil) if err1 != nil { require.Fail(t, err1.Error()) } + explainStr1 := explain1.String() fmt.Println(explainStr1) 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"+ @@ -329,10 +348,11 @@ func TestQueryConnectionSGRules(t *testing.T) { // 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) - explainStr2, err2 := vpcConfig.ExplainConnectivity(vsi3a, vsi1, connectionUDP1) + explain2, err2 := vpcConfig.ExplainConnectivity(vsi3a, vsi1, connectionUDP1) if err2 != nil { require.Fail(t, err2.Error()) } + explainStr2 := explain2.String() fmt.Println(explainStr2) 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"+ @@ -344,10 +364,11 @@ func TestQueryConnectionSGRules(t *testing.T) { // 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) - explainStr3, err3 := vpcConfig.ExplainConnectivity(vsi3a, vsi1, connectionTCP1) + explain3, err3 := vpcConfig.ExplainConnectivity(vsi3a, vsi1, connectionTCP1) if err3 != nil { require.Fail(t, err3.Error()) } + explainStr3 := explain3.String() fmt.Println(explainStr3) 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"+ @@ -362,10 +383,11 @@ func TestQueryConnectionSGRules(t *testing.T) { // 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) - explainStr4, err4 := vpcConfig.ExplainConnectivity(vsi3a, vsi1, connectionTCP2) + explain4, err4 := vpcConfig.ExplainConnectivity(vsi3a, vsi1, connectionTCP2) if err4 != nil { require.Fail(t, err4.Error()) } + explainStr4 := explain4.String() fmt.Println(explainStr4) 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"+ diff --git a/pkg/vpcmodel/debugOutput.go b/pkg/vpcmodel/debugOutput.go index 2290e0b98..da64fe605 100644 --- a/pkg/vpcmodel/debugOutput.go +++ b/pkg/vpcmodel/debugOutput.go @@ -10,8 +10,9 @@ func (t *DebugOutputFormatter) WriteOutput(c1, c2 *VPCConfig, cfgsDiff *diffBetweenCfgs, outFile string, grouping bool, - uc OutputUseCase) (*SingleAnalysisOutput, error) { - out, err := headerOfAnalyzedVPC(uc, c1.VPC.Name(), "", c1) + uc OutputUseCase, + explanation *Explanation) (*SingleAnalysisOutput, error) { + out, err := headerOfAnalyzedVPC(uc, c1.VPC.Name(), "", c1, explanation) hasStatelessConns := false if err != nil { return nil, err diff --git a/pkg/vpcmodel/drawioOutput.go b/pkg/vpcmodel/drawioOutput.go index 1e85ace6b..600bf3b04 100644 --- a/pkg/vpcmodel/drawioOutput.go +++ b/pkg/vpcmodel/drawioOutput.go @@ -147,7 +147,8 @@ func (d *DrawioOutputFormatter) WriteOutput(c1, c2 map[string]*VPCConfig, cfgsDiff *diffBetweenCfgs, outFile string, grouping bool, - uc OutputUseCase) (string, error) { + uc OutputUseCase, + explanation *Explanation) (string, error) { switch uc { case AllEndpoints: gConn := map[string]*GroupConnLines{} @@ -189,6 +190,7 @@ func (d *ArchDrawioOutputFormatter) WriteOutput(c1, c2 map[string]*VPCConfig, cfgsDiff *diffBetweenCfgs, outFile string, grouping bool, - uc OutputUseCase) (string, error) { - return d.DrawioOutputFormatter.WriteOutput(c1, c2, nil, nil, nil, outFile, grouping, uc) + uc OutputUseCase, + explanation *Explanation) (string, error) { + return d.DrawioOutputFormatter.WriteOutput(c1, c2, nil, nil, nil, outFile, grouping, uc, explanation) } diff --git a/pkg/vpcmodel/jsonOutput.go b/pkg/vpcmodel/jsonOutput.go index 42f3f2507..8e80e2917 100644 --- a/pkg/vpcmodel/jsonOutput.go +++ b/pkg/vpcmodel/jsonOutput.go @@ -17,7 +17,8 @@ func (j *JSONoutputFormatter) WriteOutput(c1, c2 *VPCConfig, cfgsDiff *diffBetweenCfgs, outFile string, grouping bool, - uc OutputUseCase) (*SingleAnalysisOutput, error) { + uc OutputUseCase, + explanation *Explanation) (*SingleAnalysisOutput, error) { var all interface{} switch uc { case AllEndpoints: diff --git a/pkg/vpcmodel/mdOutput.go b/pkg/vpcmodel/mdOutput.go index bc9ee5fb7..298aa6a14 100644 --- a/pkg/vpcmodel/mdOutput.go +++ b/pkg/vpcmodel/mdOutput.go @@ -28,13 +28,14 @@ func (m *MDoutputFormatter) WriteOutput(c1, c2 *VPCConfig, cfgsDiff *diffBetweenCfgs, outFile string, grouping bool, - uc OutputUseCase) (*SingleAnalysisOutput, error) { + uc OutputUseCase, + explanation *Explanation) (*SingleAnalysisOutput, error) { // get output by analysis type v2Name := "" if c2 != nil { v2Name = c2.VPC.Name() } - out, err := headerOfAnalyzedVPC(uc, c1.VPC.Name(), v2Name, c1) + out, err := headerOfAnalyzedVPC(uc, c1.VPC.Name(), v2Name, c1, explanation) if err != nil { return nil, err } diff --git a/pkg/vpcmodel/nodesExplainability.go b/pkg/vpcmodel/nodesExplainability.go index 3c16fb29e..5980ab273 100644 --- a/pkg/vpcmodel/nodesExplainability.go +++ b/pkg/vpcmodel/nodesExplainability.go @@ -30,16 +30,51 @@ type srcDstDetails struct { type rulesAndConnDetails []*srcDstDetails -type explanation struct { +type ExplanationArgs struct { + src string + dst string + protocol string + srcMinPort int64 + srcMaxPort int64 + dstMinPort int64 + dstMaxPort int64 +} + +func NewExplanationArgs(src, dst, protocol string, srcMinPort, srcMaxPort, dstMinPort, dstMaxPort int64) *ExplanationArgs { + return &ExplanationArgs{src: src, dst: dst, protocol: protocol, + srcMinPort: srcMinPort, srcMaxPort: srcMaxPort, dstMinPort: dstMinPort, dstMaxPort: dstMaxPort} +} + +type Explanation struct { c *VPCConfig connQuery *common.ConnectionSet rulesAndDetails *rulesAndConnDetails // rules and more details for a single src->dst + src string + dst string // grouped connectivity result: // grouping common explanation lines with common src/dst (internal node) and different dst/src (external node) // [required due to computation with disjoint ip-blocks] groupedLines []*groupedConnLine } +// TODO: handle also input ICMP properties (type, code) as input args +// translates explanation args to a connection set +func (e *ExplanationArgs) getConnectionSet() *common.ConnectionSet { + if e.protocol == "" { + return nil + } + connection := common.NewConnectionSet(false) + if common.ProtocolStr(e.protocol) == common.ProtocolICMP { + connection.AddICMPConnection(common.MinICMPtype, common.MaxICMPtype, + common.MinICMPcode, common.MaxICMPcode) + } else { + connection.AddTCPorUDPConn(common.ProtocolStr(e.protocol), e.srcMinPort, + e.srcMaxPort, e.dstMinPort, e.dstMaxPort) + } + + return connection +} + // finds the node of a given, by its name, Vsi func (c *VPCConfig) getVsiNode(name string) Node { for _, node := range c.Nodes { @@ -102,31 +137,31 @@ func (c *VPCConfig) getNodesFromInput(cidrOrName string) ([]Node, error) { // ExplainConnectivity todo: this will not be needed here once we connect explanbility to the cli // nil conn means connection is not part of the query -func (c *VPCConfig) ExplainConnectivity(src, dst string, connQuery *common.ConnectionSet) (out string, err error) { +func (c *VPCConfig) ExplainConnectivity(src, dst string, connQuery *common.ConnectionSet) (res *Explanation, err error) { srcNodes, dstNodes, err := c.processInput(src, dst) if err != nil { - return "", err + return nil, err } rulesAndDetails, err1 := c.computeExplainRules(srcNodes, dstNodes, connQuery) if err1 != nil { - return "", err1 + return nil, err1 } if connQuery == nil { // find the connection between src and dst if connection not specified in query err2 := rulesAndDetails.computeConnections(c) if err2 != nil { - return "", err2 + return nil, err2 } } err3 := c.computeRouterAndActualRules(&rulesAndDetails) if err3 != nil { - return "", err3 + return nil, err3 } groupedLines, err4 := newGroupConnExplainability(c, &rulesAndDetails) if err4 != nil { - return "", err4 + return nil, err4 } - res := &explanation{c, connQuery, &rulesAndDetails, groupedLines.GroupedLines} - return res.String(), nil + + return &Explanation{c, connQuery, &rulesAndDetails, src, dst, groupedLines.GroupedLines}, nil } // computeExplainRules computes the egress and ingress rules contributing to the (existing or missing) connection @@ -299,7 +334,7 @@ func (explanationStruct *rulesAndConnDetails) String(c *VPCConfig, connQuery *co return resStr, nil } -func (explanation *explanation) String() string { +func (explanation *Explanation) String() string { linesStr := make([]string, len(explanation.groupedLines)) groupedLines := explanation.groupedLines for i, line := range groupedLines { diff --git a/pkg/vpcmodel/output.go b/pkg/vpcmodel/output.go index 6e03a35fa..f18ea7d10 100644 --- a/pkg/vpcmodel/output.go +++ b/pkg/vpcmodel/output.go @@ -51,9 +51,11 @@ type OutputGenerator struct { nodesConn map[string]*VPCConnectivity subnetsConn map[string]*VPCsubnetConnectivity cfgsDiff *diffBetweenCfgs + explanation *Explanation } -func NewOutputGenerator(c1, c2 map[string]*VPCConfig, grouping bool, uc OutputUseCase, archOnly bool) (*OutputGenerator, error) { +func NewOutputGenerator(c1, c2 map[string]*VPCConfig, grouping bool, uc OutputUseCase, + archOnly bool, explanationArgs *ExplanationArgs) (*OutputGenerator, error) { res := &OutputGenerator{ config1: c1, config2: c2, @@ -94,6 +96,14 @@ func NewOutputGenerator(c1, c2 map[string]*VPCConfig, grouping bool, uc OutputUs } res.cfgsDiff = configsDiff } + if uc == Explain { + connQuery := explanationArgs.getConnectionSet() + explanation, err := c1[i].ExplainConnectivity(explanationArgs.src, explanationArgs.dst, connQuery) + if err != nil { + return nil, err + } + res.explanation = explanation + } } } return res, nil @@ -124,7 +134,8 @@ func (o *OutputGenerator) Generate(f OutFormat, outFile string) (string, error) default: return "", errors.New("unsupported output format") } - return formatter.WriteOutput(o.config1, o.config2, o.nodesConn, o.subnetsConn, o.cfgsDiff, outFile, o.outputGrouping, o.useCase) + return formatter.WriteOutput(o.config1, o.config2, o.nodesConn, o.subnetsConn, o.cfgsDiff, + outFile, o.outputGrouping, o.useCase, o.explanation) } // SingleVpcOutputFormatter is an interface for a formatter that can handle only one vpc @@ -132,7 +143,7 @@ func (o *OutputGenerator) Generate(f OutFormat, outFile string) (string, error) type SingleVpcOutputFormatter interface { WriteOutput(c1, c2 *VPCConfig, conn *VPCConnectivity, subnetsConn *VPCsubnetConnectivity, subnetsDiff *diffBetweenCfgs, - outFile string, grouping bool, uc OutputUseCase) (*SingleAnalysisOutput, error) + outFile string, grouping bool, uc OutputUseCase, explainStruct *Explanation) (*SingleAnalysisOutput, error) } // OutputFormatter is an interface for formatter that handle multi vpcs. @@ -140,7 +151,7 @@ type SingleVpcOutputFormatter interface { type OutputFormatter interface { WriteOutput(c1, c2 map[string]*VPCConfig, conn map[string]*VPCConnectivity, subnetsConn map[string]*VPCsubnetConnectivity, subnetsDiff *diffBetweenCfgs, - outFile string, grouping bool, uc OutputUseCase) (string, error) + outFile string, grouping bool, uc OutputUseCase, explainStruct *Explanation) (string, error) } // serialOutputFormatter is the formatter for json, md and txt formats. @@ -168,14 +179,15 @@ func (of *serialOutputFormatter) createSingleVpcFormatter() SingleVpcOutputForma func (of *serialOutputFormatter) WriteOutput(c1, c2 map[string]*VPCConfig, conns map[string]*VPCConnectivity, subnetsConns map[string]*VPCsubnetConnectivity, subnetsDiff *diffBetweenCfgs, - outFile string, grouping bool, uc OutputUseCase) (string, error) { - diffAnalysis := uc == EndpointsDiff || uc == SubnetsDiff - if !diffAnalysis { + outFile string, grouping bool, uc OutputUseCase, explainStruct *Explanation) (string, error) { + singleVPCAnalysis := uc == EndpointsDiff || uc == SubnetsDiff || uc == Explain + if !singleVPCAnalysis { outputPerVPC := make([]*SingleAnalysisOutput, len(c1)) i := 0 for name := range c1 { vpcAnalysisOutput, err2 := - of.createSingleVpcFormatter().WriteOutput(c1[name], nil, conns[name], subnetsConns[name], subnetsDiff, "", grouping, uc) + of.createSingleVpcFormatter().WriteOutput(c1[name], nil, conns[name], subnetsConns[name], + subnetsDiff, "", grouping, uc, explainStruct) if err2 != nil { return "", err2 } @@ -184,14 +196,15 @@ func (of *serialOutputFormatter) WriteOutput(c1, c2 map[string]*VPCConfig, conns } return of.AggregateVPCsOutput(outputPerVPC, uc, outFile) } - // its a diff mode, we have only one vpc on each map: + // its diff or explain mode, we have only one vpc on each map: name, _ := common.AnyMapEntry(c1) vpcAnalysisOutput, err2 := - of.createSingleVpcFormatter().WriteOutput(c1[name], c2[name], conns[name], subnetsConns[name], subnetsDiff, "", grouping, uc) + of.createSingleVpcFormatter().WriteOutput(c1[name], c2[name], conns[name], subnetsConns[name], + subnetsDiff, "", grouping, uc, explainStruct) if err2 != nil { return "", err2 } - return of.WriteDiffOutput(vpcAnalysisOutput, uc, outFile) + return of.WriteDiffOrExplainOutput(vpcAnalysisOutput, uc, outFile) } func WriteToFile(content, fileName string) (string, error) { @@ -247,7 +260,7 @@ func (of *serialOutputFormatter) AggregateVPCsOutput(outputList []*SingleAnalysi } // WriteDiffOutput actual writing the output into file, with required format adjustments -func (of *serialOutputFormatter) WriteDiffOutput(output *SingleAnalysisOutput, uc OutputUseCase, outFile string) (string, error) { +func (of *serialOutputFormatter) WriteDiffOrExplainOutput(output *SingleAnalysisOutput, uc OutputUseCase, outFile string) (string, error) { var res string var err error switch of.outFormat { diff --git a/pkg/vpcmodel/textOutput.go b/pkg/vpcmodel/textOutput.go index 3c81c3663..60c87b2cd 100644 --- a/pkg/vpcmodel/textOutput.go +++ b/pkg/vpcmodel/textOutput.go @@ -8,7 +8,7 @@ import ( type TextOutputFormatter struct { } -func headerOfAnalyzedVPC(uc OutputUseCase, vpcName, vpc2Name string, c1 *VPCConfig) (string, error) { +func headerOfAnalyzedVPC(uc OutputUseCase, vpcName, vpc2Name string, c1 *VPCConfig, explanation *Explanation) (string, error) { switch uc { case AllEndpoints, AllSubnets, SingleSubnet: if c1.IsMultipleVPCsConfig { @@ -21,6 +21,8 @@ func headerOfAnalyzedVPC(uc OutputUseCase, vpcName, vpc2Name string, c1 *VPCConf return fmt.Sprintf("Connectivity for VPC %s\n", vpcName), nil case SubnetsDiff, EndpointsDiff: return fmt.Sprintf("Connectivity diff between VPC %s and VPC %s\n", vpcName, vpc2Name), nil + case Explain: + return fmt.Sprintf("Connectivity explanation between %s and %s\n", explanation.src, explanation.dst), nil } return "", nil // should never get here } @@ -31,13 +33,14 @@ func (t *TextOutputFormatter) WriteOutput(c1, c2 *VPCConfig, cfgsDiff *diffBetweenCfgs, outFile string, grouping bool, - uc OutputUseCase) (*SingleAnalysisOutput, error) { + uc OutputUseCase, + explanation *Explanation) (*SingleAnalysisOutput, error) { vpc2Name := "" if c2 != nil { vpc2Name = c2.VPC.Name() } // header line - specify the VPC analyzed - out, err := headerOfAnalyzedVPC(uc, c1.VPC.Name(), vpc2Name, c1) + out, err := headerOfAnalyzedVPC(uc, c1.VPC.Name(), vpc2Name, c1, explanation) if err != nil { return nil, err } @@ -56,6 +59,8 @@ func (t *TextOutputFormatter) WriteOutput(c1, c2 *VPCConfig, case SubnetsDiff, EndpointsDiff: out += cfgsDiff.String() hasStatelessConns = cfgsDiff.hasStatelessConns() + case Explain: + out += explanation.String() } // write output to file and return the output string _, err = WriteToFile(out, outFile)