diff --git a/cmd/common.go b/cmd/common.go index 8e6566f..0f9debd 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -235,17 +235,19 @@ func loadConfig(nodetype check.NodeType) string { } func mapToBenchmarkVersion(kubeToBenchmarkMap map[string]string, kv string) (string, error) { + kvOriginal := kv cisVersion, found := kubeToBenchmarkMap[kv] + glog.V(2).Info(fmt.Sprintf("mapToBenchmarkVersion for k8sVersion: %q cisVersion: %q found: %t\n", kv, cisVersion, found)) for !found && (kv != defaultKubeVersion && !isEmpty(kv)) { kv = decrementVersion(kv) cisVersion, found = kubeToBenchmarkMap[kv] - glog.V(2).Info(fmt.Sprintf("mapToBenchmarkVersion for cisVersion: %q found: %t\n", cisVersion, found)) + glog.V(2).Info(fmt.Sprintf("mapToBenchmarkVersion for k8sVersion: %q cisVersion: %q found: %t\n", kv, cisVersion, found)) } if !found { - glog.V(1).Info(fmt.Sprintf("mapToBenchmarkVersion unable to find a match for: %q", kv)) + glog.V(1).Info(fmt.Sprintf("mapToBenchmarkVersion unable to find a match for: %q", kvOriginal)) glog.V(3).Info(fmt.Sprintf("mapToBenchmarkVersion kubeToBenchmarkSMap: %#v", kubeToBenchmarkMap)) - return "", fmt.Errorf("Unable to find a matching Benchmark Version match for kubernetes version: %s", kubeVersion) + return "", fmt.Errorf("unable to find a matching Benchmark Version match for kubernetes version: %s", kvOriginal) } return cisVersion, nil @@ -285,6 +287,8 @@ func getBenchmarkVersion(kubeVersion, benchmarkVersion string, v *viper.Viper) ( glog.V(2).Info(fmt.Sprintf("Mapped Kubernetes version: %s to Benchmark version: %s", kubeVersion, benchmarkVersion)) } + + glog.V(1).Info(fmt.Sprintf("Kubernetes version: %q to Benchmark version: %q", kubeVersion, benchmarkVersion)) return benchmarkVersion, nil } diff --git a/cmd/common_test.go b/cmd/common_test.go index 68bc36b..85e60a4 100644 --- a/cmd/common_test.go +++ b/cmd/common_test.go @@ -186,15 +186,16 @@ func TestMapToCISVersion(t *testing.T) { kubeVersion string succeed bool exp string + expErr string }{ - {kubeVersion: "1.9", succeed: false, exp: ""}, + {kubeVersion: "1.9", succeed: false, exp: "", expErr: "unable to find a matching Benchmark Version match for kubernetes version: 1.9"}, {kubeVersion: "1.11", succeed: true, exp: "cis-1.3"}, {kubeVersion: "1.12", succeed: true, exp: "cis-1.3"}, {kubeVersion: "1.13", succeed: true, exp: "cis-1.4"}, {kubeVersion: "1.16", succeed: true, exp: "cis-1.4"}, {kubeVersion: "ocp-3.10", succeed: true, exp: "rh-0.7"}, {kubeVersion: "ocp-3.11", succeed: true, exp: "rh-0.7"}, - {kubeVersion: "unknown", succeed: false, exp: ""}, + {kubeVersion: "unknown", succeed: false, exp: "", expErr: "unable to find a matching Benchmark Version match for kubernetes version: unknown"}, } for _, c := range cases { rv, err := mapToBenchmarkVersion(kubeToBenchmarkMap, c.kubeVersion) @@ -210,9 +211,14 @@ func TestMapToCISVersion(t *testing.T) { if c.exp != rv { t.Errorf("[%q]- expected %q but Got %q", c.kubeVersion, c.exp, rv) } + } else { if c.exp != rv { - t.Errorf("mapToBenchmarkVersion kubeversion: %q Got %q expected %s", c.kubeVersion, rv, c.exp) + t.Errorf("[%q]-mapToBenchmarkVersion kubeversion: %q Got %q expected %s", c.kubeVersion, c.kubeVersion, rv, c.exp) + } + + if c.expErr != err.Error() { + t.Errorf("[%q]-mapToBenchmarkVersion expected Error: %q instead Got %q", c.kubeVersion, c.expErr, err.Error()) } } } diff --git a/cmd/kubernetes_version.go b/cmd/kubernetes_version.go new file mode 100644 index 0000000..be0ff8b --- /dev/null +++ b/cmd/kubernetes_version.go @@ -0,0 +1,142 @@ +package cmd + +import ( + "crypto/tls" + "encoding/json" + "encoding/pem" + "fmt" + "io/ioutil" + "net/http" + "os" + "strings" + + "github.com/golang/glog" +) + +func getKubeVersionFromRESTAPI() (string, error) { + k8sVersionURL := getKubernetesURL() + serviceaccount := "/var/run/secrets/kubernetes.io/serviceaccount" + cacertfile := fmt.Sprintf("%s/ca.crt", serviceaccount) + tokenfile := fmt.Sprintf("%s/token", serviceaccount) + + tlsCert, err := loadCertficate(cacertfile) + if err != nil { + return "", err + } + + tb, err := ioutil.ReadFile(tokenfile) + if err != nil { + return "", err + } + token := strings.TrimSpace(string(tb)) + + data, err := getWebData(k8sVersionURL, token, tlsCert) + if err != nil { + return "", err + } + + k8sVersion, err := extractVersion(data) + if err != nil { + return "", err + } + return k8sVersion, nil +} + +func extractVersion(data []byte) (string, error) { + type versionResponse struct { + Major string + Minor string + GitVersion string + GitCommit string + GitTreeState string + BuildDate string + GoVersion string + Compiler string + Platform string + } + + vrObj := &versionResponse{} + glog.V(2).Info(fmt.Sprintf("vd: %s\n", string(data))) + err := json.Unmarshal(data, vrObj) + if err != nil { + return "", err + } + glog.V(2).Info(fmt.Sprintf("vrObj: %#v\n", vrObj)) + + // Some provides return the minor version like "15+" + minor := strings.Replace(vrObj.Minor, "+", "", -1) + ver := fmt.Sprintf("%s.%s", vrObj.Major, minor) + return ver, nil +} + +func getWebData(srvURL, token string, cacert *tls.Certificate) ([]byte, error) { + glog.V(2).Info(fmt.Sprintf("getWebData srvURL: %s\n", srvURL)) + + tlsConf := &tls.Config{ + Certificates: []tls.Certificate{*cacert}, + InsecureSkipVerify: true, + } + tr := &http.Transport{ + TLSClientConfig: tlsConf, + } + client := &http.Client{Transport: tr} + req, err := http.NewRequest(http.MethodGet, srvURL, nil) + if err != nil { + return nil, err + } + + authToken := fmt.Sprintf("Bearer %s", token) + glog.V(2).Info(fmt.Sprintf("getWebData AUTH TOKEN --[%q]--\n", authToken)) + req.Header.Set("Authorization", authToken) + + resp, err := client.Do(req) + if err != nil { + glog.V(2).Info(fmt.Sprintf("HTTP ERROR: %v\n", err)) + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + glog.V(2).Info(fmt.Sprintf("URL:[%s], StatusCode:[%d] \n Headers: %#v\n", srvURL, resp.StatusCode, resp.Header)) + err = fmt.Errorf("URL:[%s], StatusCode:[%d]", srvURL, resp.StatusCode) + return nil, err + } + + return ioutil.ReadAll(resp.Body) +} + +func loadCertficate(certFile string) (*tls.Certificate, error) { + cacert, err := ioutil.ReadFile(certFile) + if err != nil { + return nil, err + } + + var tlsCert tls.Certificate + block, _ := pem.Decode(cacert) + if block == nil { + return nil, fmt.Errorf("unable to Decode certificate") + } + + glog.V(2).Info(fmt.Sprintf("Loading CA certificate")) + tlsCert.Certificate = append(tlsCert.Certificate, block.Bytes) + return &tlsCert, nil +} + +func getKubernetesURL() string { + k8sVersionURL := "https://kubernetes.default.svc/version" + + // The following provides flexibility to use + // K8S provided variables is situations where + // hostNetwork: true + if !isEmpty(os.Getenv("KUBE_BENCH_K8S_ENV")) { + k8sHost := os.Getenv("KUBERNETES_SERVICE_HOST") + k8sPort := os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS") + if !isEmpty(k8sHost) && !isEmpty(k8sPort) { + return fmt.Sprintf("https://%s:%s/version", k8sHost, k8sPort) + } + + glog.V(2).Info(fmt.Sprintf("KUBE_BENCH_K8S_ENV is set, but environment variables KUBERNETES_SERVICE_HOST or KUBERNETES_SERVICE_PORT_HTTPS are not set")) + } + + return k8sVersionURL +} diff --git a/cmd/kubernetes_version_test.go b/cmd/kubernetes_version_test.go new file mode 100644 index 0000000..1513f62 --- /dev/null +++ b/cmd/kubernetes_version_test.go @@ -0,0 +1,233 @@ +package cmd + +import ( + "crypto/tls" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "strconv" + "testing" +) + +func TestLoadCertficate(t *testing.T) { + tmp, err := ioutil.TempDir("", "TestFakeLoadCertficate") + if err != nil { + t.Fatalf("unable to create temp directory: %v", err) + } + defer os.RemoveAll(tmp) + + goodCertFile, _ := ioutil.TempFile(tmp, "good-cert-*") + _, _ = goodCertFile.Write([]byte(`-----BEGIN CERTIFICATE----- +MIICyDCCAbCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwprdWJl +cm5ldGVzMB4XDTE5MTEwODAxNDAwMFoXDTI5MTEwNTAxNDAwMFowFTETMBEGA1UE +AxMKa3ViZXJuZXRlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMn6 +wjvhMc9e0MDwpQNhp8SPxmv1DsYJ4Btp1GeScIgKKDwppuoOmVizLiMNdV5+70yI +MgNfm/gwFRNDOtN3R7msfZDD5Dd1vI6qRTP21DFOGVdysFdwqJTs0nGcmfvZEOtw +9cjcsXrBi2Mg54v+X/pq2w51xajCGBt2+bpxJJ3WBiWqKYv0RQdNL0WZGm+V9BuP +pHRWPBeLxuCzt5K3Gx+1QDy8o6Y4sSRPssWC4RhD9Hs5/9eeGRyZslLs+AuqdDLQ +aziiSjHVtgCfRXE9nYVxaDIwTFuh+Q1IvtB36NRLyX47oya+BbX3PoCtSjA36RBb +tcJfulr3oNHnb2ZlfcUCAwEAAaMjMCEwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB +/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAAeQDkbM6DilLkIVQDyxauETgJDV +2AaVzYaAgDApQGAoYV6WIY7Exk4TlmLeKQjWt2s/GtthQWuzUDKTcEvWcG6gNdXk +gzuCRRDMGu25NtG3m67w4e2RzW8Z/lzvbfyJZGoV2c6dN+yP9/Pw2MXlrnMWugd1 +jLv3UYZRHMpuNS8BJU74BuVzVPHd55RAl+bV8yemdZJ7pPzMvGbZ7zRXWODTDlge +CQb9lY+jYErisH8Sq7uABFPvi7RaTh8SS7V7OxqHZvmttNTdZs4TIkk45JK7Y+Xq +FAjB57z2NcIgJuVpQnGRYtr/JcH2Qdsq8bLtXaojUIWOOqoTDRLYozdMOOQ= +-----END CERTIFICATE-----`)) + badCertFile, _ := ioutil.TempFile(tmp, "bad-cert-*") + + cases := []struct { + file string + fail bool + }{ + { + file: "missing cert file", + fail: true, + }, + { + file: badCertFile.Name(), + fail: true, + }, + { + file: goodCertFile.Name(), + fail: false, + }, + } + + for id, c := range cases { + t.Run(strconv.Itoa(id), func(t *testing.T) { + tlsCert, err := loadCertficate(c.file) + if !c.fail { + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if tlsCert == nil { + t.Errorf("missing returned TLS Certificate") + } + } else { + if err == nil { + t.Errorf("Expected error") + } + } + + }) + } +} + +func TestGetWebData(t *testing.T) { + okfn := func(w http.ResponseWriter, r *http.Request) { + _, _ = fmt.Fprintln(w, `{ + "major": "1", + "minor": "15"}`) + } + errfn := func(w http.ResponseWriter, r *http.Request) { + http.Error(w, http.StatusText(http.StatusInternalServerError), + http.StatusInternalServerError) + } + token := "dummyToken" + var tlsCert tls.Certificate + + cases := []struct { + fn http.HandlerFunc + fail bool + }{ + { + fn: okfn, + fail: false, + }, + { + fn: errfn, + fail: true, + }, + } + + for id, c := range cases { + t.Run(strconv.Itoa(id), func(t *testing.T) { + ts := httptest.NewServer(c.fn) + defer ts.Close() + data, err := getWebData(ts.URL, token, &tlsCert) + if !c.fail { + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if len(data) == 0 { + t.Errorf("missing data") + } + } else { + if err == nil { + t.Errorf("Expected error") + } + } + }) + } + +} + +func TestExtractVersion(t *testing.T) { + okJSON := []byte(`{ + "major": "1", + "minor": "15", + "gitVersion": "v1.15.3", + "gitCommit": "2d3c76f9091b6bec110a5e63777c332469e0cba2", + "gitTreeState": "clean", + "buildDate": "2019-08-20T18:57:36Z", + "goVersion": "go1.12.9", + "compiler": "gc", + "platform": "linux/amd64" + }`) + + invalidJSON := []byte(`{ + "major": "1", + "minor": "15", + "gitVersion": "v1.15.3", + "gitCommit": "2d3c76f9091b6bec110a5e63777c332469e0cba2", + "gitTreeState": "clean",`) + + cases := []struct { + data []byte + fail bool + expectedVer string + }{ + { + data: okJSON, + fail: false, + expectedVer: "1.15", + }, + { + data: invalidJSON, + fail: true, + }, + } + + for id, c := range cases { + t.Run(strconv.Itoa(id), func(t *testing.T) { + ver, err := extractVersion(c.data) + if !c.fail { + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if c.expectedVer != ver { + t.Errorf("Expected %q but Got %q", c.expectedVer, ver) + } + } else { + if err == nil { + t.Errorf("Expected error") + } + } + }) + } +} + +func TestGetKubernetesURL(t *testing.T) { + + resetEnvs := func() { + os.Unsetenv("KUBE_BENCH_K8S_ENV") + os.Unsetenv("KUBERNETES_SERVICE_HOST") + os.Unsetenv("KUBERNETES_SERVICE_PORT_HTTPS") + } + + setEnvs := func() { + os.Setenv("KUBE_BENCH_K8S_ENV", "1") + os.Setenv("KUBERNETES_SERVICE_HOST", "testHostServer") + os.Setenv("KUBERNETES_SERVICE_PORT_HTTPS", "443") + } + + cases := []struct { + useDefault bool + expected string + }{ + { + useDefault: true, + expected: "https://kubernetes.default.svc/version", + }, + { + useDefault: false, + expected: "https://testHostServer:443/version", + }, + } + for id, c := range cases { + t.Run(strconv.Itoa(id), func(t *testing.T) { + resetEnvs() + defer resetEnvs() + if !c.useDefault { + setEnvs() + } + k8sURL := getKubernetesURL() + + if !c.useDefault { + if k8sURL != c.expected { + t.Errorf("Expected %q but Got %q", k8sURL, c.expected) + } + } else { + if k8sURL != c.expected { + t.Errorf("Expected %q but Got %q", k8sURL, c.expected) + } + } + }) + } + +} diff --git a/cmd/util.go b/cmd/util.go index c55e753..2c2967e 100644 --- a/cmd/util.go +++ b/cmd/util.go @@ -78,12 +78,14 @@ func cleanIDs(list string) map[string]bool { func ps(proc string) string { // TODO: truncate proc to 15 chars // See https://github.com/aquasecurity/kube-bench/issues/328#issuecomment-506813344 + glog.V(2).Info(fmt.Sprintf("ps - proc: %q", proc)) cmd := exec.Command("/bin/ps", "-C", proc, "-o", "cmd", "--no-headers") out, err := cmd.Output() if err != nil { continueWithError(fmt.Errorf("%s: %s", cmd.Args, err), "") } + glog.V(2).Info(fmt.Sprintf("ps - returning: %q", string(out))) return string(out) } @@ -206,7 +208,9 @@ func verifyBin(bin string) bool { // but apiserver is not a match for kube-apiserver reFirstWord := regexp.MustCompile(`^(\S*\/)*` + bin) lines := strings.Split(out, "\n") + glog.V(2).Info(fmt.Sprintf("verifyBin - lines(%d)", len(lines))) for _, l := range lines { + glog.V(2).Info(fmt.Sprintf("reFirstWord.Match(%s)\n\n\n\n", l)) if reFirstWord.Match([]byte(l)) { return true } @@ -271,6 +275,12 @@ Alternatively, you can specify the version with --version ` func getKubeVersion() (string, error) { + + if k8sVer, err := getKubeVersionFromRESTAPI(); err == nil { + glog.V(2).Info(fmt.Sprintf("Kubernetes REST API Reported version: %s", k8sVer)) + return k8sVer, nil + } + // These executables might not be on the user's path. _, err := exec.LookPath("kubectl")