Repository: aquasecurity/kube-bench
Pull request #497 (2019-10-27 18:25:04)

Fixes Issue #331: Changes the Error Message When Programs are Missing  
Fixes Issue #331.

- When **kubectl** and **kubelet** are missing, the following error is displayed:

  ``` 
   Version check failed:  
    Unable to find the programs kubectl or kubelet in the PATH.
    These programs are used to determine version of kubernetes.
    Make sure the /usr/bin directory is mapped to the container,
    either using the job.yaml file used, or docker command.

    For job.yaml:
    ...
    - name: usr-bin
       mountPath: /usr/bin
    ...

    For docker command:
         docker -v $(which kubectl):/usr/bin/kubectl ....

    Alternatively, you can specify the version with --version
         kube-bench --version <VERSION> ...


- When component programs are missing, the following error is displayed:

   ```
   Unable to find control plane programs (apiserver, controllermanager, etc...)
   These program names are provided in the config.yaml, section master.apiserver.bins
   The following control plane programs for the component 'apiserver' have been search,
    but none of them have been found:
	  - kube-apiserver
	  - hyperkube apiserver
	  - hyperkube kube-apiserver
	  - apiserver
   ```




codecov[bot]: # [Codecov](https://codecov.io/gh/aquasecurity/kube-bench/pull/497?src=pr&el=h1) Report
> Merging [#497](https://codecov.io/gh/aquasecurity/kube-bench/pull/497?src=pr&el=desc) into [master](https://codecov.io/gh/aquasecurity/kube-bench/commit/91bd47f2964cc6f1f9e15ae9ac638f6dcba7f780?src=pr&el=desc) will **increase** coverage by `0.05%`.
> The diff coverage is `62.96%`.

[![Impacted file tree graph](https://codecov.io/gh/aquasecurity/kube-bench/pull/497/graphs/tree.svg?width=650&token=MVaDEmdgi9&height=150&src=pr)](https://codecov.io/gh/aquasecurity/kube-bench/pull/497?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #497      +/-   ##
==========================================
+ Coverage   61.69%   61.74%   +0.05%     
==========================================
  Files          10       10              
  Lines         825      847      +22     
==========================================
+ Hits          509      523      +14     
- Misses        290      297       +7     
- Partials       26       27       +1
```


| [Impacted Files](https://codecov.io/gh/aquasecurity/kube-bench/pull/497?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [cmd/common.go](https://codecov.io/gh/aquasecurity/kube-bench/pull/497/diff?src=pr&el=tree#diff-Y21kL2NvbW1vbi5nbw==) | `21.29% <50%> (ø)` | :arrow_up: |
| [cmd/util.go](https://codecov.io/gh/aquasecurity/kube-bench/pull/497/diff?src=pr&el=tree#diff-Y21kL3V0aWwuZ28=) | `67.01% <64%> (-0.44%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/aquasecurity/kube-bench/pull/497?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/aquasecurity/kube-bench/pull/497?src=pr&el=footer). Last update [91bd47f...44423fe](https://codecov.io/gh/aquasecurity/kube-bench/pull/497?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/cmd/common.go b/cmd/common.go
index 0ea5d9f..ed5918b 100644
--- a/cmd/common.go
+++ b/cmd/common.go
@@ -81,7 +81,7 @@ func runChecks(nodetype check.NodeType) {
 
 	// Get the set of executables and config files we care about on this type of node.
 	typeConf := viper.Sub(string(nodetype))
-	binmap, err := getBinaries(typeConf)
+	binmap, err := getBinaries(typeConf, nodetype)
 
 	// Checks that the executables we need for the node type are running.
 	if err != nil {
@@ -213,7 +213,7 @@ func loadConfig(nodetype check.NodeType) string {
 	if kubeVersion == "" {
 		runningVersion, err = getKubeVersion()
 		if err != nil {
-			exitWithError(fmt.Errorf("Version check failed: %s\nAlternatively, you can specify the version with --version", err))
+			exitWithError(fmt.Errorf("Version check failed: \n%s", err))
 		}
 	}
 
@@ -245,7 +245,7 @@ func isMaster() bool {
 		glog.V(2).Info("No master components found to be running")
 		return false
 	}
-	components, err := getBinariesFunc(masterConf)
+	components, err := getBinariesFunc(masterConf, check.MASTER)
 
 	if err != nil {
 		glog.V(2).Info(err)
diff --git a/cmd/util.go b/cmd/util.go
index 184c479..4d439d9 100644
--- a/cmd/util.go
+++ b/cmd/util.go
@@ -27,7 +27,7 @@ var (
 
 var psFunc func(string) string
 var statFunc func(string) (os.FileInfo, error)
-var getBinariesFunc func(*viper.Viper) (map[string]string, error)
+var getBinariesFunc func(*viper.Viper, check.NodeType) (map[string]string, error)
 var TypeMap = map[string][]string{
 	"ca":         []string{"cafile", "defaultcafile"},
 	"kubeconfig": []string{"kubeconfig", "defaultkubeconfig"},
@@ -89,7 +89,7 @@ func ps(proc string) string {
 
 // getBinaries finds which of the set of candidate executables are running.
 // It returns an error if one mandatory executable is not running.
-func getBinaries(v *viper.Viper) (map[string]string, error) {
+func getBinaries(v *viper.Viper, nodetype check.NodeType) (map[string]string, error) {
 	binmap := make(map[string]string)
 
 	for _, component := range v.GetStringSlice("components") {
@@ -103,7 +103,8 @@ func getBinaries(v *viper.Viper) (map[string]string, error) {
 		if len(bins) > 0 {
 			bin, err := findExecutable(bins)
 			if err != nil && !optional {
-				return nil, fmt.Errorf("need %s executable but none of the candidates are running", component)
+				glog.Warning(buildComponentMissingErrorMessage(nodetype, component, bins))
+				return nil, fmt.Errorf("unable to detect running programs for component %q", component)
 			}
 
 			// Default the executable name that we'll substitute to the name of the component
@@ -269,6 +270,25 @@ func multiWordReplace(s string, subname string, sub string) string {
 	return strings.Replace(s, subname, sub, -1)
 }
 
+const missingKubectlKubeletMessage = `
+Unable to find the programs kubectl or kubelet in the PATH.
+These programs are used to determine which version of Kubernetes is running.
+Make sure the /usr/bin directory is mapped to the container, 
+either in the job.yaml file, or Docker command.
+
+For job.yaml:
+...
+- name: usr-bin
+  mountPath: /usr/bin
+...
+
+For docker command:
+   docker -v $(which kubectl):/usr/bin/kubectl ....
+
+Alternatively, you can specify the version with --version
+   kube-bench --version <VERSION> ...
+`
+
 func getKubeVersion() (string, error) {
 	// These executables might not be on the user's path.
 	_, err := exec.LookPath("kubectl")
@@ -282,7 +302,9 @@ func getKubeVersion() (string, error) {
 			if err == nil {
 				return getVersionFromKubeletOutput(string(out)), nil
 			}
-			return "", fmt.Errorf("need kubectl or kubelet binaries to get kubernetes version")
+
+			glog.Warning(missingKubectlKubeletMessage)
+			return "", fmt.Errorf("unable to find the programs kubectl or kubelet in the PATH")
 		}
 		return getKubeVersionFromKubelet(), nil
 	}
@@ -344,3 +366,29 @@ func makeSubstitutions(s string, ext string, m map[string]string) string {
 
 	return s
 }
+
+func buildComponentMissingErrorMessage(nodetype check.NodeType, component string, bins []string) string {
+
+	errMessageTemplate := `
+Unable to detect running programs for component %q
+The following %q programs have been searched, but none of them have been found:
+%s
+
+These program names are provided in the config.yaml, section '%s.%s.bins'
+`
+
+	componentRoleName := "master node"
+	componentType := "master"
+
+	if nodetype == check.NODE {
+		componentRoleName = "worker node"
+		componentType = "node"
+	}
+
+	binList := ""
+	for _, bin := range bins {
+		binList = fmt.Sprintf("%s\t- %s\n", binList, bin)
+	}
+
+	return fmt.Sprintf(errMessageTemplate, component, componentRoleName, binList, componentType, component)
+}


Test patch:
diff --git a/cmd/common_test.go b/cmd/common_test.go
index 2819637..722a73e 100644
--- a/cmd/common_test.go
+++ b/cmd/common_test.go
@@ -16,10 +16,11 @@ package cmd
 
 import (
 	"errors"
+	"testing"
+
 	"github.com/aquasecurity/kube-bench/check"
 	"github.com/spf13/viper"
 	"github.com/stretchr/testify/assert"
-	"testing"
 )
 
 func TestNewRunFilter(t *testing.T) {
@@ -114,44 +115,44 @@ func TestNewRunFilter(t *testing.T) {
 }
 
 func TestIsMaster(t *testing.T) {
-	testCases := []struct{
-		name string
-		cfgFile string
-		getBinariesFunc func(*viper.Viper) (map[string]string, error)
-		isMaster bool
+	testCases := []struct {
+		name            string
+		cfgFile         string
+		getBinariesFunc func(*viper.Viper, check.NodeType) (map[string]string, error)
+		isMaster        bool
 	}{
 		{
-			name: "valid config, is master and all components are running",
+			name:    "valid config, is master and all components are running",
 			cfgFile: "../cfg/config.yaml",
-			getBinariesFunc: func(viper *viper.Viper) (strings map[string]string, i error) {
+			getBinariesFunc: func(viper *viper.Viper, nt check.NodeType) (strings map[string]string, i error) {
 				return map[string]string{"apiserver": "kube-apiserver"}, nil
 			},
 			isMaster: true,
 		},
 		{
-			name: "valid config, is master and but not all components are running",
+			name:    "valid config, is master and but not all components are running",
 			cfgFile: "../cfg/config.yaml",
-			getBinariesFunc: func(viper *viper.Viper) (strings map[string]string, i error) {
+			getBinariesFunc: func(viper *viper.Viper, nt check.NodeType) (strings map[string]string, i error) {
 				return map[string]string{}, nil
 			},
 			isMaster: false,
 		},
 		{
-			name: "valid config, is master, not all components are running and fails to find all binaries",
+			name:    "valid config, is master, not all components are running and fails to find all binaries",
 			cfgFile: "../cfg/config.yaml",
-			getBinariesFunc: func(viper *viper.Viper) (strings map[string]string, i error) {
+			getBinariesFunc: func(viper *viper.Viper, nt check.NodeType) (strings map[string]string, i error) {
 				return map[string]string{}, errors.New("failed to find binaries")
 			},
 			isMaster: false,
 		},
 		{
-			name: "valid config, does not include master",
-			cfgFile: "../cfg/node_only.yaml",
+			name:     "valid config, does not include master",
+			cfgFile:  "../cfg/node_only.yaml",
 			isMaster: false,
 		},
 	}
 
-	for _, tc := range testCases{
+	for _, tc := range testCases {
 		cfgFile = tc.cfgFile
 		initConfig()
 
diff --git a/cmd/util_test.go b/cmd/util_test.go
index c761988..6fb6901 100644
--- a/cmd/util_test.go
+++ b/cmd/util_test.go
@@ -22,6 +22,7 @@ import (
 	"strconv"
 	"testing"
 
+	"github.com/aquasecurity/kube-bench/check"
 	"github.com/spf13/viper"
 )
 
@@ -166,7 +167,7 @@ func TestGetBinaries(t *testing.T) {
 			for k, val := range c.config {
 				v.Set(k, val)
 			}
-			m, err := getBinaries(v)
+			m, err := getBinaries(v, check.MASTER)
 			if c.expectErr {
 				if err == nil {
 					t.Fatal("Got nil Expected error")