diff --git a/README.md b/README.md index 788981fc..12a1f00b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,124 @@ # netpol-analyzer -A Golang library for analyzing connectivity-configuration resources (a.k.a. network policies) + +## About netpol-analyzer +This repo contains a Golang library and CLI for analyzing k8s connectivity-configuration resources (a.k.a. network policies). + + +## CLI usage + +### Evaluate command +``` +Evaluate if a specific connection allowed + +Usage: + k8snetpolicy evaluate [flags] + +Aliases: + evaluate, eval, check, allow + +Examples: + # Evaluate if a specific connection is allowed on given resources from dir path + k8snetpolicy eval --dirpath ./resources_dir/ -s pod-1 -d pod-2 -p 80 + + # Evaluate if a specific connection is allowed on a live k8s cluster + k8snetpolicy eval -k ./kube/config -s pod-1 -d pod-2 -p 80 + +Flags: + --destination-ip string Destination (external) IP address + --destination-namespace string Destination pod namespace (default "default") + -d, --destination-pod string Destination pod name + -p, --destination-port string Destination port (name or number) + -h, --help help for evaluate + --protocol string Protocol in use (tcp, udp, sctp) (default "tcp") + --source-ip string Source (external) IP address + -n, --source-namespace string Source pod namespace (default "default") + -s, --source-pod string Source pod name, required + +Global Flags: + -c, --context string Kubernetes context to use when evaluating connections in a live cluster + --dirpath string Resources dir path when evaluating connections from a dir + -k, --kubeconfig string Path and file to use for kubeconfig when evaluating connections in a live cluster (default "/home/adisos/.kube/config") +``` + +### List command +``` +Lists all allowed connections based on the workloads and network policies +defined + +Usage: + k8snetpolicy list [flags] + +Examples: + # Get list of allowed connections from resources dir path + k8snetpolicy list --dirpath ./resources_dir/ + + # Get list of allowed connections from live k8s cluster + k8snetpolicy list -k ./kube/config + +Flags: + -h, --help help for list + +Global Flags: + -c, --context string Kubernetes context to use when evaluating connections in a live cluster + --dirpath string Resources dir path when evaluating connections from a dir + -k, --kubeconfig string Path and file to use for kubeconfig when evaluating connections in a live cluster (default "/home/adisos/.kube/config") +``` + + + +### Example outputs: +``` +$ k8snetpolicy eval --dirpath tests/onlineboutique -s adservice-77d5cd745d-t8mx4 -d emailservice-54c7c5d9d-vp27n -p 80 + +default/adservice-77d5cd745d-t8mx4 => default/emailservice-54c7c5d9d-vp27n over tcp/80: false + + + +$ k8snetpolicy list --dirpath tests/onlineboutique_workloads + +0.0.0.0-255.255.255.255 => default/redis-cart[Deployment] : All Connections +default/adservice[Deployment] => default/adservice[Deployment] : All Connections +default/cartservice[Deployment] => default/cartservice[Deployment] : All Connections +default/checkoutservice[Deployment] => default/cartservice[Deployment] : TCP 7070 +default/checkoutservice[Deployment] => default/checkoutservice[Deployment] : All Connections +default/checkoutservice[Deployment] => default/currencyservice[Deployment] : TCP 7000 +default/checkoutservice[Deployment] => default/emailservice[Deployment] : TCP 8080 +default/checkoutservice[Deployment] => default/paymentservice[Deployment] : TCP 50051 +default/checkoutservice[Deployment] => default/productcatalogservice[Deployment] : TCP 3550 +default/checkoutservice[Deployment] => default/shippingservice[Deployment] : TCP 50051 +default/currencyservice[Deployment] => default/currencyservice[Deployment] : All Connections +default/emailservice[Deployment] => default/emailservice[Deployment] : All Connections +default/frontend[Deployment] => default/adservice[Deployment] : TCP 9555 +default/frontend[Deployment] => default/cartservice[Deployment] : TCP 7070 +default/frontend[Deployment] => default/checkoutservice[Deployment] : TCP 5050 +default/frontend[Deployment] => default/currencyservice[Deployment] : TCP 7000 +default/frontend[Deployment] => default/frontend[Deployment] : All Connections +default/frontend[Deployment] => default/productcatalogservice[Deployment] : TCP 3550 +default/frontend[Deployment] => default/recommendationservice[Deployment] : TCP 8080 +default/frontend[Deployment] => default/shippingservice[Deployment] : TCP 50051 +default/loadgenerator[Deployment] => default/frontend[Deployment] : TCP 8080 +default/paymentservice[Deployment] => default/paymentservice[Deployment] : All Connections +default/productcatalogservice[Deployment] => default/productcatalogservice[Deployment] : All Connections +default/recommendationservice[Deployment] => default/productcatalogservice[Deployment] : TCP 3550 +default/recommendationservice[Deployment] => default/recommendationservice[Deployment] : All Connections +default/redis-cart[Deployment] => 0.0.0.0-255.255.255.255 : All Connections +default/redis-cart[Deployment] => default/redis-cart[Deployment] : All Connections +default/shippingservice[Deployment] => default/shippingservice[Deployment] : All Connections + +``` + +## Build the project + +Make sure you have golang 1.18+ on your platform + +```commandline +git clone git@github.com:np-guard/netpol-analyzer.git +cd netpol-analyzer +make mod +make build +``` + +Test your build by running `./bin/k8snetpolicy -h`. + + + diff --git a/cmd/netpolicy/cmd/command_test.go b/cmd/netpolicy/cmd/command_test.go new file mode 100644 index 00000000..3d462d63 --- /dev/null +++ b/cmd/netpolicy/cmd/command_test.go @@ -0,0 +1,159 @@ +package cmd + +import ( + _ "embed" + "errors" + "io" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + stdoutFile *os.File + stderrFile *os.File + testOutR *os.File + testOutW *os.File + testErrR *os.File + testErrW *os.File + + //go:embed tests_outputs/test_legal_list.txt + testLegalListOutput string +) + +func preTestRun() { + stdoutFile = os.Stdout + stderrFile = os.Stderr + testOutR, testOutW, _ = os.Pipe() + os.Stdout = testOutW + testErrR, testErrW, _ = os.Pipe() + os.Stderr = testErrW +} + +// finalize test and get its output +func postTestRun(isErr bool) string { + testOutW.Close() + testErrW.Close() + out, _ := io.ReadAll(testOutR) + errOut, _ := io.ReadAll(testErrR) + os.Stdout = stdoutFile + os.Stderr = stderrFile + actualOutput := string(out) + actualErr := string(errOut) + if isErr { + return actualErr + } + return actualOutput +} + +func runTest(test cmdTest, t *testing.T) { + // run the test and get its output + preTestRun() + rootCmd.SetArgs(test.args) + err := rootCmd.Execute() + if !test.isErr { + require.Nilf(t, err, "expected no errors, but got %v", err) + } else { + require.NotNil(t, err, "expected error, but got no error") + } + actual := postTestRun(test.isErr) + + // compare actual to test.expectedOutput + if test.exact { + assert.Equal(t, test.expectedOutput, actual, "error - unexpected output") + } else if test.containment { + isContained := strings.Contains(actual, test.expectedOutput) + assert.True(t, isContained, "test %s error: %s not contained in %s", test.name, test.expectedOutput, actual) + } else { + assert.Error(t, errors.New(""), "test %s: missing containment or equality flag for test") + } +} + +type cmdTest struct { + name string + args []string + expectedOutput string + exact bool + containment bool + isErr bool +} + +func TestCommannds(t *testing.T) { + tests := []cmdTest{ + { + name: "test_illegal_command", + args: []string{"A"}, + expectedOutput: "Error: unknown command \"A\" for \"k8snetpolicy\"", + containment: true, + isErr: true, + }, + + { + name: "test_illegal_eval_no_args", + args: []string{"eval"}, + expectedOutput: "no source defined", + containment: true, + isErr: true, + }, + + { + name: "test_illegal_eval_peer_not_found", + args: []string{ + "eval", + "--dirpath", + filepath.Join(getTestsDir(), "onlineboutique"), + "-s", + "default/adservice-77d5cd745d-t8mx4", + "-d", + "default/emailservice-54c7c5d9d-vp27n", + "-p", + "80"}, + expectedOutput: "could not find peer default/default/adservice-77d5cd745d", + containment: true, + isErr: true, + }, + + { + name: "test_legal_eval", + args: []string{ + "eval", + "--dirpath", + filepath.Join(getTestsDir(), "onlineboutique"), + "-s", + "adservice-77d5cd745d-t8mx4", + "-d", + "emailservice-54c7c5d9d-vp27n", + "-p", + "80"}, + expectedOutput: "default/adservice-77d5cd745d-t8mx4 => default/emailservice-54c7c5d9d-vp27n over tcp/80: false\n", + exact: true, + isErr: false, + }, + + { + name: "test_legal_list", + args: []string{ + "list", + "--dirpath", + filepath.Join(getTestsDir(), "onlineboutique"), + }, + expectedOutput: testLegalListOutput, + exact: true, + isErr: false, + }, + } + + for _, test := range tests { + runTest(test, t) + } +} + +func getTestsDir() string { + currentDir, _ := os.Getwd() + res := filepath.Join(currentDir, "..", "..", "..", "tests") + return res +} diff --git a/cmd/netpolicy/cmd/evaluate.go b/cmd/netpolicy/cmd/evaluate.go index 26c5473e..a1a6fd6c 100644 --- a/cmd/netpolicy/cmd/evaluate.go +++ b/cmd/netpolicy/cmd/evaluate.go @@ -48,10 +48,10 @@ var evaluateCmd = &cobra.Command{ Short: "Evaluate if a specific connection allowed", Aliases: []string{"eval", "check", "allow"}, // TODO: close on fewer, consider changing command name? Example: ` # Evaluate if a specific connection is allowed on given resources from dir path - k8snetpolicy eval --dirpath ./resources_dir/ -s default/pod-1 -d default/pod-2 -p 80 + k8snetpolicy eval --dirpath ./resources_dir/ -s pod-1 -d pod-2 -p 80 # Evaluate if a specific connection is allowed on a live k8s cluster - k8snetpolicy eval -k ./kube/config -s default/pod-1 -d default/pod-2 -p 80`, + k8snetpolicy eval -k ./kube/config -s pod-1 -d pod-2 -p 80`, // TODO: can this check be done in an Args function (e.g., incl. built-in's such as MinArgs(3))? PersistentPreRunE: func(cmd *cobra.Command, args []string) error { diff --git a/cmd/netpolicy/cmd/tests_outputs/test_legal_list.txt b/cmd/netpolicy/cmd/tests_outputs/test_legal_list.txt new file mode 100644 index 00000000..896ac8b4 --- /dev/null +++ b/cmd/netpolicy/cmd/tests_outputs/test_legal_list.txt @@ -0,0 +1,29 @@ +0.0.0.0-255.255.255.255 => default/redis-cart-78746d49dc[ReplicaSet] : All Connections +default/adservice-77d5cd745d[ReplicaSet] => default/adservice-77d5cd745d[ReplicaSet] : All Connections +default/cartservice-74f56fd4b[ReplicaSet] => default/cartservice-74f56fd4b[ReplicaSet] : All Connections +default/checkoutservice-69c8ff664b[ReplicaSet] => default/cartservice-74f56fd4b[ReplicaSet] : TCP 7070 +default/checkoutservice-69c8ff664b[ReplicaSet] => default/checkoutservice-69c8ff664b[ReplicaSet] : All Connections +default/checkoutservice-69c8ff664b[ReplicaSet] => default/currencyservice-77654bbbdd[ReplicaSet] : TCP 7000 +default/checkoutservice-69c8ff664b[ReplicaSet] => default/emailservice-54c7c5d9d[ReplicaSet] : TCP 8080 +default/checkoutservice-69c8ff664b[ReplicaSet] => default/paymentservice-bbcbdc6b6[ReplicaSet] : TCP 50051 +default/checkoutservice-69c8ff664b[ReplicaSet] => default/productcatalogservice-68765d49b6[ReplicaSet] : TCP 3550 +default/checkoutservice-69c8ff664b[ReplicaSet] => default/shippingservice-5bd985c46d[ReplicaSet] : TCP 50051 +default/currencyservice-77654bbbdd[ReplicaSet] => default/currencyservice-77654bbbdd[ReplicaSet] : All Connections +default/emailservice-54c7c5d9d[ReplicaSet] => default/emailservice-54c7c5d9d[ReplicaSet] : All Connections +default/frontend-99684f7f8[ReplicaSet] => default/adservice-77d5cd745d[ReplicaSet] : TCP 9555 +default/frontend-99684f7f8[ReplicaSet] => default/cartservice-74f56fd4b[ReplicaSet] : TCP 7070 +default/frontend-99684f7f8[ReplicaSet] => default/checkoutservice-69c8ff664b[ReplicaSet] : TCP 5050 +default/frontend-99684f7f8[ReplicaSet] => default/currencyservice-77654bbbdd[ReplicaSet] : TCP 7000 +default/frontend-99684f7f8[ReplicaSet] => default/frontend-99684f7f8[ReplicaSet] : All Connections +default/frontend-99684f7f8[ReplicaSet] => default/productcatalogservice-68765d49b6[ReplicaSet] : TCP 3550 +default/frontend-99684f7f8[ReplicaSet] => default/recommendationservice-5f8c456796[ReplicaSet] : TCP 8080 +default/frontend-99684f7f8[ReplicaSet] => default/shippingservice-5bd985c46d[ReplicaSet] : TCP 50051 +default/loadgenerator-555fbdc87d[ReplicaSet] => default/frontend-99684f7f8[ReplicaSet] : TCP 8080 +default/loadgenerator-555fbdc87d[ReplicaSet] => default/loadgenerator-555fbdc87d[ReplicaSet] : All Connections +default/paymentservice-bbcbdc6b6[ReplicaSet] => default/paymentservice-bbcbdc6b6[ReplicaSet] : All Connections +default/productcatalogservice-68765d49b6[ReplicaSet] => default/productcatalogservice-68765d49b6[ReplicaSet] : All Connections +default/recommendationservice-5f8c456796[ReplicaSet] => default/productcatalogservice-68765d49b6[ReplicaSet] : TCP 3550 +default/recommendationservice-5f8c456796[ReplicaSet] => default/recommendationservice-5f8c456796[ReplicaSet] : All Connections +default/redis-cart-78746d49dc[ReplicaSet] => 0.0.0.0-255.255.255.255 : All Connections +default/redis-cart-78746d49dc[ReplicaSet] => default/redis-cart-78746d49dc[ReplicaSet] : All Connections +default/shippingservice-5bd985c46d[ReplicaSet] => default/shippingservice-5bd985c46d[ReplicaSet] : All Connections \ No newline at end of file diff --git a/go.mod b/go.mod index 55d10d14..87eba5c6 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,8 @@ go 1.18 require ( github.com/hashicorp/golang-lru v0.5.4 github.com/spf13/cobra v1.5.0 - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b + github.com/stretchr/testify v1.8.1 + gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.24.2 k8s.io/apimachinery v0.24.2 k8s.io/client-go v0.24.2 @@ -34,6 +35,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect diff --git a/go.sum b/go.sum index 3afcadd4..fc2de573 100644 --- a/go.sum +++ b/go.sum @@ -245,12 +245,17 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -611,8 +616,9 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=