diff --git a/cmd/common.go b/cmd/common.go deleted file mode 100644 index 54b8b33..0000000 --- a/cmd/common.go +++ /dev/null @@ -1,180 +0,0 @@ -// Copyright © 2017 Aqua Security Software Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package cmd - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - - "github.com/aquasecurity/kube-bench/check" - "github.com/golang/glog" - "github.com/spf13/viper" -) - -var ( - errmsgs string -) - -func runChecks(nodetype check.NodeType) { - var summary check.Summary - var file string - var err error - var typeConf *viper.Viper - - switch nodetype { - case check.MASTER: - file = masterFile - case check.NODE: - file = nodeFile - case check.FEDERATED: - file = federatedFile - } - - runningVersion, err := getKubeVersion() - if err != nil && kubeVersion == "" { - exitWithError(fmt.Errorf("Version check failed: %s\nAlternatively, you can specify the version with --version", err)) - } - path, err := getConfigFilePath(kubeVersion, runningVersion, file) - if err != nil { - exitWithError(fmt.Errorf("can't find %s controls file in %s: %v", nodetype, cfgDir, err)) - } - - def := filepath.Join(path, file) - in, err := ioutil.ReadFile(def) - if err != nil { - exitWithError(fmt.Errorf("error opening %s controls file: %v", nodetype, err)) - } - - glog.V(1).Info(fmt.Sprintf("Using benchmark file: %s\n", def)) - - // Merge kubernetes version specific config if any. - viper.SetConfigFile(path + "/config.yaml") - err = viper.MergeInConfig() - if err != nil { - if os.IsNotExist(err) { - glog.V(2).Info(fmt.Sprintf("No version-specific config.yaml file in %s", path)) - } else { - exitWithError(fmt.Errorf("couldn't read config file %s: %v", path+"/config.yaml", err)) - } - } else { - glog.V(1).Info(fmt.Sprintf("Using config file: %s\n", viper.ConfigFileUsed())) - } - - // Get the set of exectuables and config files we care about on this type of node. This also - // checks that the executables we need for the node type are running. - typeConf = viper.Sub(string(nodetype)) - binmap := getBinaries(typeConf) - confmap := getConfigFiles(typeConf) - - // Variable substitutions. Replace all occurrences of variables in controls files. - s := string(in) - s = makeSubstitutions(s, "bin", binmap) - s = makeSubstitutions(s, "conf", confmap) - - controls, err := check.NewControls(nodetype, []byte(s)) - if err != nil { - exitWithError(fmt.Errorf("error setting up %s controls: %v", nodetype, err)) - } - - if groupList != "" && checkList == "" { - ids := cleanIDs(groupList) - summary = controls.RunGroup(ids...) - } else if checkList != "" && groupList == "" { - ids := cleanIDs(checkList) - summary = controls.RunChecks(ids...) - } else if checkList != "" && groupList != "" { - exitWithError(fmt.Errorf("group option and check option can't be used together")) - } else { - summary = controls.RunGroup() - } - - // if we successfully ran some tests and it's json format, ignore the warnings - if (summary.Fail > 0 || summary.Warn > 0 || summary.Pass > 0) && jsonFmt { - out, err := controls.JSON() - if err != nil { - exitWithError(fmt.Errorf("failed to output in JSON format: %v", err)) - } - - fmt.Println(string(out)) - } else { - // if we want to store in PostgreSQL, convert to JSON and save it - if (summary.Fail > 0 || summary.Warn > 0 || summary.Pass > 0) && pgSQL { - out, err := controls.JSON() - if err != nil { - exitWithError(fmt.Errorf("failed to output in JSON format: %v", err)) - } - - savePgsql(string(out)) - } else { - prettyPrint(controls, summary) - } - } -} - -// colorPrint outputs the state in a specific colour, along with a message string -func colorPrint(state check.State, s string) { - colors[state].Printf("[%s] ", state) - fmt.Printf("%s", s) -} - -// prettyPrint outputs the results to stdout in human-readable format -func prettyPrint(r *check.Controls, summary check.Summary) { - // Print check results. - if !noResults { - colorPrint(check.INFO, fmt.Sprintf("%s %s\n", r.ID, r.Text)) - for _, g := range r.Groups { - colorPrint(check.INFO, fmt.Sprintf("%s %s\n", g.ID, g.Text)) - for _, c := range g.Checks { - colorPrint(c.State, fmt.Sprintf("%s %s\n", c.ID, c.Text)) - } - } - - fmt.Println() - } - - // Print remediations. - if !noRemediations { - if summary.Fail > 0 || summary.Warn > 0 { - colors[check.WARN].Printf("== Remediations ==\n") - for _, g := range r.Groups { - for _, c := range g.Checks { - if c.State != check.PASS { - fmt.Printf("%s %s\n", c.ID, c.Remediation) - } - } - } - fmt.Println() - } - } - - // Print summary setting output color to highest severity. - if !noSummary { - var res check.State - if summary.Fail > 0 { - res = check.FAIL - } else if summary.Warn > 0 { - res = check.WARN - } else { - res = check.PASS - } - - colors[res].Printf("== Summary ==\n") - fmt.Printf("%d checks PASS\n%d checks FAIL\n%d checks WARN\n", - summary.Pass, summary.Fail, summary.Warn, - ) - } -} diff --git a/cmd/database.go b/cmd/database.go deleted file mode 100644 index dbbbb94..0000000 --- a/cmd/database.go +++ /dev/null @@ -1,60 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - "time" - - "github.com/golang/glog" - "github.com/jinzhu/gorm" - _ "github.com/jinzhu/gorm/dialects/postgres" // database packages get blank imports - "github.com/spf13/viper" -) - -func savePgsql(jsonInfo string) { - envVars := map[string]string{ - "PGSQL_HOST": viper.GetString("PGSQL_HOST"), - "PGSQL_USER": viper.GetString("PGSQL_USER"), - "PGSQL_DBNAME": viper.GetString("PGSQL_DBNAME"), - "PGSQL_SSLMODE": viper.GetString("PGSQL_SSLMODE"), - "PGSQL_PASSWORD": viper.GetString("PGSQL_PASSWORD"), - } - - for k, v := range envVars { - if v == "" { - exitWithError(fmt.Errorf("environment variable %s is missing", envVarsPrefix+"_"+k)) - } - } - - connInfo := fmt.Sprintf("host=%s user=%s dbname=%s sslmode=%s password=%s", - envVars["PGSQL_HOST"], - envVars["PGSQL_USER"], - envVars["PGSQL_DBNAME"], - envVars["PGSQL_SSLMODE"], - envVars["PGSQL_PASSWORD"], - ) - - hostname, err := os.Hostname() - if err != nil { - exitWithError(fmt.Errorf("received error looking up hostname: %s", err)) - } - - timestamp := time.Now() - - type ScanResult struct { - gorm.Model - ScanHost string `gorm:"type:varchar(63) not null"` // https://www.ietf.org/rfc/rfc1035.txt - ScanTime time.Time `gorm:"not null"` - ScanInfo string `gorm:"type:jsonb not null"` - } - - db, err := gorm.Open("postgres", connInfo) - defer db.Close() - if err != nil { - exitWithError(fmt.Errorf("received error connecting to database: %s", err)) - } - - db.Debug().AutoMigrate(&ScanResult{}) - db.Save(&ScanResult{ScanHost: hostname, ScanTime: timestamp, ScanInfo: jsonInfo}) - glog.V(2).Info(fmt.Sprintf("successfully stored result to: %s", envVars["PGSQL_HOST"])) -} diff --git a/cmd/federated.go b/cmd/federated.go deleted file mode 100644 index 0f9dbf3..0000000 --- a/cmd/federated.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright © 2017 Aqua Security Software Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package cmd - -import ( - "github.com/aquasecurity/kube-bench/check" - "github.com/spf13/cobra" -) - -// nodeCmd represents the node command -var federatedCmd = &cobra.Command{ - Use: "federated", - Short: "Run benchmark checks for a Kubernetes federated deployment.", - Long: `Run benchmark checks for a Kubernetes federated deployment.`, - Run: func(cmd *cobra.Command, args []string) { - runChecks(check.FEDERATED) - }, -} - -func init() { - federatedCmd.PersistentFlags().StringVarP(&federatedFile, - "file", - "f", - "/federated.yaml", - "Alternative YAML file for federated checks", - ) - - RootCmd.AddCommand(federatedCmd) -} diff --git a/cmd/master.go b/cmd/master.go deleted file mode 100644 index 1659d38..0000000 --- a/cmd/master.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright © 2017 Aqua Security Software Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package cmd - -import ( - "github.com/aquasecurity/kube-bench/check" - "github.com/spf13/cobra" -) - -// masterCmd represents the master command -var masterCmd = &cobra.Command{ - Use: "master", - Short: "Run benchmark checks for a Kubernetes master node.", - Long: `Run benchmark checks for a Kubernetes master node.`, - Run: func(cmd *cobra.Command, args []string) { - runChecks(check.MASTER) - }, -} - -func init() { - masterCmd.PersistentFlags().StringVarP(&masterFile, - "file", - "f", - "/master.yaml", - "Alternative YAML file for master checks", - ) - - RootCmd.AddCommand(masterCmd) -} diff --git a/cmd/node.go b/cmd/node.go deleted file mode 100644 index b07ed7e..0000000 --- a/cmd/node.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright © 2017 Aqua Security Software Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package cmd - -import ( - "github.com/aquasecurity/kube-bench/check" - "github.com/spf13/cobra" -) - -// nodeCmd represents the node command -var nodeCmd = &cobra.Command{ - Use: "node", - Short: "Run benchmark checks for a Kubernetes node.", - Long: `Run benchmark checks for a Kubernetes node.`, - Run: func(cmd *cobra.Command, args []string) { - runChecks(check.NODE) - }, -} - -func init() { - nodeCmd.PersistentFlags().StringVarP(&nodeFile, - "file", - "f", - "/node.yaml", - "Alternative YAML file for node checks", - ) - - RootCmd.AddCommand(nodeCmd) -} diff --git a/cmd/root.go b/cmd/root.go deleted file mode 100644 index a41ea61..0000000 --- a/cmd/root.go +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright © 2017 Aqua Security Software Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package cmd - -import ( - goflag "flag" - "fmt" - "os" - - "github.com/aquasecurity/kube-bench/check" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -var ( - envVarsPrefix = "KUBE_BENCH" - defaultKubeVersion = "1.6" - kubeVersion string - cfgFile string - cfgDir string - jsonFmt bool - pgSQL bool - checkList string - groupList string - masterFile string - nodeFile string - federatedFile string - noResults bool - noSummary bool - noRemediations bool -) - -// RootCmd represents the base command when called without any subcommands -var RootCmd = &cobra.Command{ - Use: os.Args[0], - Short: "Run CIS Benchmarks checks against a Kubernetes deployment", - Long: `This tool runs the CIS Kubernetes Benchmark (http://www.cisecurity.org/benchmark/kubernetes/)`, -} - -// Execute adds all child commands to the root command sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - goflag.Set("logtostderr", "true") - goflag.CommandLine.Parse([]string{}) - - if err := RootCmd.Execute(); err != nil { - fmt.Println(err) - os.Exit(-1) - } -} - -func init() { - cobra.OnInitialize(initConfig) - - // Output control - RootCmd.PersistentFlags().BoolVar(&noResults, "noresults", false, "Disable printing of results section") - RootCmd.PersistentFlags().BoolVar(&noSummary, "nosummary", false, "Disable printing of summary section") - RootCmd.PersistentFlags().BoolVar(&noRemediations, "noremediations", false, "Disable printing of remediations section") - RootCmd.PersistentFlags().BoolVar(&jsonFmt, "json", false, "Prints the results as JSON") - RootCmd.PersistentFlags().BoolVar(&pgSQL, "pgsql", false, "Save the results to PostgreSQL") - - RootCmd.PersistentFlags().StringVarP( - &checkList, - "check", - "c", - "", - `A comma-delimited list of checks to run as specified in CIS document. Example --check="1.1.1,1.1.2"`, - ) - RootCmd.PersistentFlags().StringVarP( - &groupList, - "group", - "g", - "", - `Run all the checks under this comma-delimited list of groups. Example --group="1.1"`, - ) - RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is ./cfg/config.yaml)") - RootCmd.PersistentFlags().StringVarP(&cfgDir, "config-dir", "D", "./cfg/", "config directory") - RootCmd.PersistentFlags().StringVar(&kubeVersion, "version", "", "Manually specify Kubernetes version, automatically detected if unset") - - goflag.CommandLine.VisitAll(func(goflag *goflag.Flag) { - RootCmd.PersistentFlags().AddGoFlag(goflag) - }) - -} - -// initConfig reads in config file and ENV variables if set. -func initConfig() { - if cfgFile != "" { // enable ability to specify config file via flag - viper.SetConfigFile(cfgFile) - } else { - viper.SetConfigName("config") // name of config file (without extension) - viper.AddConfigPath(cfgDir) // adding ./cfg as first search path - } - - viper.SetEnvPrefix(envVarsPrefix) - viper.AutomaticEnv() // read in environment variables that match - - // If a config file is found, read it in. - if err := viper.ReadInConfig(); err != nil { - colorPrint(check.FAIL, fmt.Sprintf("Failed to read config file: %v\n", err)) - os.Exit(1) - } -} diff --git a/cmd/util.go b/cmd/util.go deleted file mode 100644 index 4c62841..0000000 --- a/cmd/util.go +++ /dev/null @@ -1,338 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "regexp" - "strconv" - "strings" - - "github.com/aquasecurity/kube-bench/check" - "github.com/fatih/color" - "github.com/golang/glog" - "github.com/spf13/viper" -) - -var ( - // Print colors - colors = map[check.State]*color.Color{ - check.PASS: color.New(color.FgGreen), - check.FAIL: color.New(color.FgRed), - check.WARN: color.New(color.FgYellow), - check.INFO: color.New(color.FgBlue), - } -) - -var psFunc func(string) string -var statFunc func(string) (os.FileInfo, error) - -func init() { - psFunc = ps - statFunc = os.Stat -} - -func printlnWarn(msg string) { - fmt.Fprintf(os.Stderr, "[%s] %s\n", - colors[check.WARN].Sprintf("%s", check.WARN), - msg, - ) -} - -func sprintlnWarn(msg string) string { - return fmt.Sprintf("[%s] %s", - colors[check.WARN].Sprintf("%s", check.WARN), - msg, - ) -} - -func exitWithError(err error) { - fmt.Fprintf(os.Stderr, "\n%v\n", err) - os.Exit(1) -} - -func continueWithError(err error, msg string) string { - if err != nil { - glog.V(2).Info(err) - } - - if msg != "" { - fmt.Fprintf(os.Stderr, "%s\n", msg) - } - - return "" -} - -func cleanIDs(list string) []string { - list = strings.Trim(list, ",") - ids := strings.Split(list, ",") - - for _, id := range ids { - id = strings.Trim(id, " ") - } - - return ids -} - -// ps execs out to the ps command; it's separated into a function so we can write tests -func ps(proc string) string { - cmd := exec.Command("ps", "-C", proc, "-o", "cmd", "--no-headers") - out, err := cmd.Output() - if err != nil { - continueWithError(fmt.Errorf("%s: %s", cmd.Args, err), "") - } - - return string(out) -} - -// getBinaries finds which of the set of candidate executables are running -func getBinaries(v *viper.Viper) map[string]string { - binmap := make(map[string]string) - - for _, component := range v.GetStringSlice("components") { - s := v.Sub(component) - if s == nil { - continue - } - - optional := s.GetBool("optional") - bins := s.GetStringSlice("bins") - if len(bins) > 0 { - bin, err := findExecutable(bins) - if err != nil && !optional { - exitWithError(fmt.Errorf("need %s executable but none of the candidates are running", component)) - } - - // Default the executable name that we'll substitute to the name of the component - if bin == "" { - bin = component - glog.V(2).Info(fmt.Sprintf("Component %s not running", component)) - } else { - glog.V(2).Info(fmt.Sprintf("Component %s uses running binary %s", component, bin)) - } - binmap[component] = bin - } - } - - return binmap -} - -// getConfigFilePath locates the config files we should be using based on either the specified -// version, or the running version of kubernetes if not specified -func getConfigFilePath(specifiedVersion string, runningVersion string, filename string) (path string, err error) { - var fileVersion string - - if specifiedVersion != "" { - fileVersion = specifiedVersion - } else { - fileVersion = runningVersion - } - - glog.V(2).Info(fmt.Sprintf("Looking for config for version %s", fileVersion)) - - for { - path = filepath.Join(cfgDir, fileVersion) - file := filepath.Join(path, string(filename)) - glog.V(2).Info(fmt.Sprintf("Looking for config file: %s\n", file)) - - if _, err = os.Stat(file); !os.IsNotExist(err) { - if specifiedVersion == "" && fileVersion != runningVersion { - glog.V(1).Info(fmt.Sprintf("No test file found for %s - using tests for Kubernetes %s\n", runningVersion, fileVersion)) - } - return path, nil - } - - // If we were given an explicit version to look for, don't look for any others - if specifiedVersion != "" { - return "", err - } - - fileVersion = decrementVersion(fileVersion) - if fileVersion == "" { - return "", fmt.Errorf("no test files found <= runningVersion") - } - } -} - -// decrementVersion decrements the version number -// We want to decrement individually even through versions where we don't supply test files -// just in case someone wants to specify their own test files for that version -func decrementVersion(version string) string { - split := strings.Split(version, ".") - minor, err := strconv.Atoi(split[1]) - if err != nil { - return "" - } - if minor <= 1 { - return "" - } - split[1] = strconv.Itoa(minor - 1) - return strings.Join(split, ".") -} - -// getConfigFiles finds which of the set of candidate config files exist -// accepts a string 't' which indicates the type of config file, conf, -// podspec or untifile. -func getConfigFiles(v *viper.Viper) map[string]string { - confmap := make(map[string]string) - - for _, component := range v.GetStringSlice("components") { - s := v.Sub(component) - if s == nil { - continue - } - - // See if any of the candidate config files exist - conf := findConfigFile(s.GetStringSlice("confs")) - if conf == "" { - if s.IsSet("defaultconf") { - conf = s.GetString("defaultconf") - glog.V(2).Info(fmt.Sprintf("Using default config file name '%s' for component %s", conf, component)) - } else { - // Default the config file name that we'll substitute to the name of the component - glog.V(2).Info(fmt.Sprintf("Missing config file for %s", component)) - conf = component - } - } else { - glog.V(2).Info(fmt.Sprintf("Component %s uses config file '%s'", component, conf)) - } - - confmap[component] = conf - } - - return confmap -} - -// verifyBin checks that the binary specified is running -func verifyBin(bin string) bool { - - // Strip any quotes - bin = strings.Trim(bin, "'\"") - - // bin could consist of more than one word - // We'll search for running processes with the first word, and then check the whole - // proc as supplied is included in the results - proc := strings.Fields(bin)[0] - out := psFunc(proc) - - // There could be multiple lines in the ps output - // The binary needs to be the first word in the ps output, except that it could be preceded by a path - // e.g. /usr/bin/kubelet is a match for kubelet - // but apiserver is not a match for kube-apiserver - reFirstWord := regexp.MustCompile(`^(\S*\/)*` + bin) - lines := strings.Split(out, "\n") - for _, l := range lines { - if reFirstWord.Match([]byte(l)) { - return true - } - } - - return false -} - -// fundConfigFile looks through a list of possible config files and finds the first one that exists -func findConfigFile(candidates []string) string { - for _, c := range candidates { - _, err := statFunc(c) - if err == nil { - return c - } - if !os.IsNotExist(err) { - exitWithError(fmt.Errorf("error looking for file %s: %v", c, err)) - } - } - - return "" -} - -// findExecutable looks through a list of possible executable names and finds the first one that's running -func findExecutable(candidates []string) (string, error) { - for _, c := range candidates { - if verifyBin(c) { - return c, nil - } - glog.V(1).Info(fmt.Sprintf("executable '%s' not running", c)) - } - - return "", fmt.Errorf("no candidates running") -} - -func multiWordReplace(s string, subname string, sub string) string { - f := strings.Fields(sub) - if len(f) > 1 { - sub = "'" + sub + "'" - } - - return strings.Replace(s, subname, sub, -1) -} - -func getKubeVersion() (string, error) { - // These executables might not be on the user's path. - _, err := exec.LookPath("kubectl") - - if err != nil { - _, err = exec.LookPath("kubelet") - if err != nil { - return "", fmt.Errorf("need kubectl or kubelet binaries to get kubernetes version") - } - return getKubeVersionFromKubelet(), nil - } - - return getKubeVersionFromKubectl(), nil -} - -func getKubeVersionFromKubectl() string { - cmd := exec.Command("kubectl", "version", "--short") - out, err := cmd.CombinedOutput() - if err != nil { - continueWithError(fmt.Errorf("%s", out), "") - } - - return getVersionFromKubectlOutput(string(out)) -} - -func getKubeVersionFromKubelet() string { - cmd := exec.Command("kubelet", "--version") - out, err := cmd.CombinedOutput() - - if err != nil { - continueWithError(fmt.Errorf("%s", out), "") - } - - return getVersionFromKubeletOutput(string(out)) -} - -func getVersionFromKubectlOutput(s string) string { - serverVersionRe := regexp.MustCompile(`Server Version: v(\d+.\d+)`) - subs := serverVersionRe.FindStringSubmatch(s) - if len(subs) < 2 { - printlnWarn(fmt.Sprintf("Unable to get kubectl version, using default version: %s", defaultKubeVersion)) - return defaultKubeVersion - } - return subs[1] -} - -func getVersionFromKubeletOutput(s string) string { - serverVersionRe := regexp.MustCompile(`Kubernetes v(\d+.\d+)`) - subs := serverVersionRe.FindStringSubmatch(s) - if len(subs) < 2 { - printlnWarn(fmt.Sprintf("Unable to get kubelet version, using default version: %s", defaultKubeVersion)) - return defaultKubeVersion - } - return subs[1] -} - -func makeSubstitutions(s string, ext string, m map[string]string) string { - for k, v := range m { - subst := "$" + k + ext - if v == "" { - glog.V(2).Info(fmt.Sprintf("No subsitution for '%s'\n", subst)) - continue - } - glog.V(2).Info(fmt.Sprintf("Substituting %s with '%s'\n", subst, v)) - s = multiWordReplace(s, subst, v) - } - - return s -} diff --git a/cmd/util_test.go b/cmd/util_test.go deleted file mode 100644 index 539f400..0000000 --- a/cmd/util_test.go +++ /dev/null @@ -1,352 +0,0 @@ -// Copyright © 2017 Aqua Security Software Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package cmd - -import ( - "io/ioutil" - "os" - "path/filepath" - "reflect" - "strconv" - "testing" - - "github.com/spf13/viper" -) - -var g string -var e []error -var eIndex int - -func fakeps(proc string) string { - return g -} - -func fakestat(file string) (os.FileInfo, error) { - err := e[eIndex] - eIndex++ - return nil, err -} - -func TestVerifyBin(t *testing.T) { - cases := []struct { - proc string - psOut string - exp bool - }{ - {proc: "single", psOut: "single", exp: true}, - {proc: "single", psOut: "", exp: false}, - {proc: "two words", psOut: "two words", exp: true}, - {proc: "two words", psOut: "", exp: false}, - {proc: "cmd", psOut: "cmd param1 param2", exp: true}, - {proc: "cmd param", psOut: "cmd param1 param2", exp: true}, - {proc: "cmd param", psOut: "cmd", exp: false}, - {proc: "cmd", psOut: "cmd x \ncmd y", exp: true}, - {proc: "cmd y", psOut: "cmd x \ncmd y", exp: true}, - {proc: "cmd", psOut: "/usr/bin/cmd", exp: true}, - {proc: "cmd", psOut: "kube-cmd", exp: false}, - {proc: "cmd", psOut: "/usr/bin/kube-cmd", exp: false}, - } - - psFunc = fakeps - for id, c := range cases { - t.Run(strconv.Itoa(id), func(t *testing.T) { - g = c.psOut - v := verifyBin(c.proc) - if v != c.exp { - t.Fatalf("Expected %v got %v", c.exp, v) - } - }) - } -} - -func TestFindExecutable(t *testing.T) { - cases := []struct { - candidates []string // list of executables we'd consider - psOut string // fake output from ps - exp string // the one we expect to find in the (fake) ps output - expErr bool - }{ - {candidates: []string{"one", "two", "three"}, psOut: "two", exp: "two"}, - {candidates: []string{"one", "two", "three"}, psOut: "two three", exp: "two"}, - {candidates: []string{"one double", "two double", "three double"}, psOut: "two double is running", exp: "two double"}, - {candidates: []string{"one", "two", "three"}, psOut: "blah", expErr: true}, - {candidates: []string{"one double", "two double", "three double"}, psOut: "two", expErr: true}, - {candidates: []string{"apiserver", "kube-apiserver"}, psOut: "kube-apiserver", exp: "kube-apiserver"}, - {candidates: []string{"apiserver", "kube-apiserver", "hyperkube-apiserver"}, psOut: "kube-apiserver", exp: "kube-apiserver"}, - } - - psFunc = fakeps - for id, c := range cases { - t.Run(strconv.Itoa(id), func(t *testing.T) { - g = c.psOut - e, err := findExecutable(c.candidates) - if e != c.exp { - t.Fatalf("Expected %v got %v", c.exp, e) - } - - if err == nil && c.expErr { - t.Fatalf("Expected error") - } - - if err != nil && !c.expErr { - t.Fatalf("Didn't expect error: %v", err) - } - }) - } -} - -func TestGetBinaries(t *testing.T) { - cases := []struct { - config map[string]interface{} - psOut string - exp map[string]string - }{ - { - config: map[string]interface{}{"components": []string{"apiserver"}, "apiserver": map[string]interface{}{"bins": []string{"apiserver", "kube-apiserver"}}}, - psOut: "kube-apiserver", - exp: map[string]string{"apiserver": "kube-apiserver"}, - }, - { - // "thing" is not in the list of components - config: map[string]interface{}{"components": []string{"apiserver"}, "apiserver": map[string]interface{}{"bins": []string{"apiserver", "kube-apiserver"}}, "thing": map[string]interface{}{"bins": []string{"something else", "thing"}}}, - psOut: "kube-apiserver thing", - exp: map[string]string{"apiserver": "kube-apiserver"}, - }, - { - // "anotherthing" in list of components but doesn't have a defintion - config: map[string]interface{}{"components": []string{"apiserver", "anotherthing"}, "apiserver": map[string]interface{}{"bins": []string{"apiserver", "kube-apiserver"}}, "thing": map[string]interface{}{"bins": []string{"something else", "thing"}}}, - psOut: "kube-apiserver thing", - exp: map[string]string{"apiserver": "kube-apiserver"}, - }, - { - // more than one component - config: map[string]interface{}{"components": []string{"apiserver", "thing"}, "apiserver": map[string]interface{}{"bins": []string{"apiserver", "kube-apiserver"}}, "thing": map[string]interface{}{"bins": []string{"something else", "thing"}}}, - psOut: "kube-apiserver \nthing", - exp: map[string]string{"apiserver": "kube-apiserver", "thing": "thing"}, - }, - { - // default binary to component name - config: map[string]interface{}{"components": []string{"apiserver", "thing"}, "apiserver": map[string]interface{}{"bins": []string{"apiserver", "kube-apiserver"}}, "thing": map[string]interface{}{"bins": []string{"something else", "thing"}, "optional": true}}, - psOut: "kube-apiserver \notherthing some params", - exp: map[string]string{"apiserver": "kube-apiserver", "thing": "thing"}, - }, - } - - v := viper.New() - psFunc = fakeps - - for id, c := range cases { - t.Run(strconv.Itoa(id), func(t *testing.T) { - g = c.psOut - for k, val := range c.config { - v.Set(k, val) - } - m := getBinaries(v) - if !reflect.DeepEqual(m, c.exp) { - t.Fatalf("Got %v\nExpected %v", m, c.exp) - } - }) - } -} - -func TestMultiWordReplace(t *testing.T) { - cases := []struct { - input string - sub string - subname string - output string - }{ - {input: "Here's a file with no substitutions", sub: "blah", subname: "blah", output: "Here's a file with no substitutions"}, - {input: "Here's a file with a substitution", sub: "blah", subname: "substitution", output: "Here's a file with a blah"}, - {input: "Here's a file with multi-word substitutions", sub: "multi word", subname: "multi-word", output: "Here's a file with 'multi word' substitutions"}, - {input: "Here's a file with several several substitutions several", sub: "blah", subname: "several", output: "Here's a file with blah blah substitutions blah"}, - } - for id, c := range cases { - t.Run(strconv.Itoa(id), func(t *testing.T) { - s := multiWordReplace(c.input, c.subname, c.sub) - if s != c.output { - t.Fatalf("Expected %s got %s", c.output, s) - } - }) - } -} - -func TestKubeVersionRegex(t *testing.T) { - ver := getVersionFromKubectlOutput(`Client Version: v1.8.0 - Server Version: v1.8.12 - `) - if ver != "1.8" { - t.Fatalf("Expected 1.8 got %s", ver) - } - - ver = getVersionFromKubectlOutput("Something completely different") - if ver != "1.6" { - t.Fatalf("Expected 1.6 got %s", ver) - } -} - -func TestFindConfigFile(t *testing.T) { - cases := []struct { - input []string - statResults []error - exp string - }{ - {input: []string{"myfile"}, statResults: []error{nil}, exp: "myfile"}, - {input: []string{"thisfile", "thatfile"}, statResults: []error{os.ErrNotExist, nil}, exp: "thatfile"}, - {input: []string{"thisfile", "thatfile"}, statResults: []error{os.ErrNotExist, os.ErrNotExist}, exp: ""}, - } - - statFunc = fakestat - for id, c := range cases { - t.Run(strconv.Itoa(id), func(t *testing.T) { - e = c.statResults - eIndex = 0 - conf := findConfigFile(c.input) - if conf != c.exp { - t.Fatalf("Got %s expected %s", conf, c.exp) - } - }) - } -} - -func TestGetConfigFiles(t *testing.T) { - cases := []struct { - config map[string]interface{} - exp map[string]string - statResults []error - }{ - { - config: map[string]interface{}{"components": []string{"apiserver"}, "apiserver": map[string]interface{}{"confs": []string{"apiserver", "kube-apiserver"}}}, - statResults: []error{os.ErrNotExist, nil}, - exp: map[string]string{"apiserver": "kube-apiserver"}, - }, - { - // Component "thing" isn't included in the list of components - config: map[string]interface{}{ - "components": []string{"apiserver"}, - "apiserver": map[string]interface{}{"confs": []string{"apiserver", "kube-apiserver"}}, - "thing": map[string]interface{}{"confs": []string{"/my/file/thing"}}}, - statResults: []error{os.ErrNotExist, nil}, - exp: map[string]string{"apiserver": "kube-apiserver"}, - }, - { - // More than one component - config: map[string]interface{}{ - "components": []string{"apiserver", "thing"}, - "apiserver": map[string]interface{}{"confs": []string{"apiserver", "kube-apiserver"}}, - "thing": map[string]interface{}{"confs": []string{"/my/file/thing"}}}, - statResults: []error{os.ErrNotExist, nil, nil}, - exp: map[string]string{"apiserver": "kube-apiserver", "thing": "/my/file/thing"}, - }, - { - // Default thing to specified default config - config: map[string]interface{}{ - "components": []string{"apiserver", "thing"}, - "apiserver": map[string]interface{}{"confs": []string{"apiserver", "kube-apiserver"}}, - "thing": map[string]interface{}{"confs": []string{"/my/file/thing"}, "defaultconf": "another/thing"}}, - statResults: []error{os.ErrNotExist, nil, os.ErrNotExist}, - exp: map[string]string{"apiserver": "kube-apiserver", "thing": "another/thing"}, - }, - { - // Default thing to component name - config: map[string]interface{}{ - "components": []string{"apiserver", "thing"}, - "apiserver": map[string]interface{}{"confs": []string{"apiserver", "kube-apiserver"}}, - "thing": map[string]interface{}{"confs": []string{"/my/file/thing"}}}, - statResults: []error{os.ErrNotExist, nil, os.ErrNotExist}, - exp: map[string]string{"apiserver": "kube-apiserver", "thing": "thing"}, - }, - } - - v := viper.New() - statFunc = fakestat - - for id, c := range cases { - t.Run(strconv.Itoa(id), func(t *testing.T) { - for k, val := range c.config { - v.Set(k, val) - } - e = c.statResults - eIndex = 0 - - m := getConfigFiles(v) - if !reflect.DeepEqual(m, c.exp) { - t.Fatalf("Got %v\nExpected %v", m, c.exp) - } - }) - } -} - -func TestMakeSubsitutions(t *testing.T) { - cases := []struct { - input string - subst map[string]string - exp string - }{ - {input: "Replace $thisbin", subst: map[string]string{"this": "that"}, exp: "Replace that"}, - {input: "Replace $thisbin", subst: map[string]string{"this": "that", "here": "there"}, exp: "Replace that"}, - {input: "Replace $thisbin and $herebin", subst: map[string]string{"this": "that", "here": "there"}, exp: "Replace that and there"}, - } - for _, c := range cases { - t.Run(c.input, func(t *testing.T) { - s := makeSubstitutions(c.input, "bin", c.subst) - if s != c.exp { - t.Fatalf("Got %s expected %s", s, c.exp) - } - }) - } -} - -func TestGetConfigFilePath(t *testing.T) { - var err error - cfgDir, err = ioutil.TempDir("", "kube-bench-test") - if err != nil { - t.Fatalf("Failed to create temp directory") - } - defer os.RemoveAll(cfgDir) - d := filepath.Join(cfgDir, "1.8") - err = os.Mkdir(d, 0666) - if err != nil { - t.Fatalf("Failed to create temp file") - } - ioutil.WriteFile(filepath.Join(d, "master.yaml"), []byte("hello world"), 0666) - - cases := []struct { - specifiedVersion string - runningVersion string - succeed bool - exp string - }{ - {runningVersion: "1.8", succeed: true, exp: d}, - {runningVersion: "1.9", succeed: true, exp: d}, - {runningVersion: "1.10", succeed: true, exp: d}, - {runningVersion: "1.1", succeed: false}, - {specifiedVersion: "1.8", succeed: true, exp: d}, - {specifiedVersion: "1.9", succeed: false}, - {specifiedVersion: "1.10", succeed: false}, - } - - for _, c := range cases { - t.Run(c.specifiedVersion+"-"+c.runningVersion, func(t *testing.T) { - path, err := getConfigFilePath(c.specifiedVersion, c.runningVersion, "/master.yaml") - if err != nil && c.succeed { - t.Fatalf("Error %v", err) - } - if path != c.exp { - t.Fatalf("Got %s expected %s", path, c.exp) - } - }) - } -}