github.com/castai/kvisor@v1.7.1-0.20240516114728-b3572a2607b5/cmd/linter/kubebench/common.go (about)

     1  // Copyright © 2017 Aqua Security Software Ltd. <info@aquasec.com>
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package kubebench
    16  
    17  import (
    18  	"bufio"
    19  	"encoding/json"
    20  	"fmt"
    21  	"io/ioutil"
    22  	"os"
    23  	"path/filepath"
    24  	"sort"
    25  	"strconv"
    26  	"strings"
    27  
    28  	check2 "github.com/castai/kvisor/cmd/linter/kubebench/check"
    29  	"github.com/golang/glog"
    30  	"github.com/spf13/viper"
    31  )
    32  
    33  // NewRunFilter constructs a Predicate based on FilterOpts which determines whether tested Checks should be run or not.
    34  func NewRunFilter(opts FilterOpts) (check2.Predicate, error) {
    35  	if opts.CheckList != "" && opts.GroupList != "" {
    36  		return nil, fmt.Errorf("group option and check option can't be used together")
    37  	}
    38  
    39  	var groupIDs map[string]bool
    40  	if opts.GroupList != "" {
    41  		groupIDs = cleanIDs(opts.GroupList)
    42  	}
    43  
    44  	var checkIDs map[string]bool
    45  	if opts.CheckList != "" {
    46  		checkIDs = cleanIDs(opts.CheckList)
    47  	}
    48  
    49  	return func(g *check2.Group, c *check2.Check) bool {
    50  		test := true
    51  		if len(groupIDs) > 0 {
    52  			_, ok := groupIDs[g.ID]
    53  			test = test && ok
    54  		}
    55  
    56  		if len(checkIDs) > 0 {
    57  			_, ok := checkIDs[c.ID]
    58  			test = test && ok
    59  		}
    60  
    61  		test = test && (opts.Scored && c.Scored || opts.Unscored && !c.Scored)
    62  
    63  		return test
    64  	}, nil
    65  }
    66  
    67  func runChecks(nodetype check2.NodeType, testYamlFile, detectedVersion string) {
    68  	// Verify config file was loaded into Viper during Cobra sub-command initialization.
    69  	if configFileError != nil {
    70  		colorPrint(check2.FAIL, fmt.Sprintf("Failed to read config file: %v\n", configFileError))
    71  		os.Exit(1)
    72  	}
    73  
    74  	in, err := ioutil.ReadFile(testYamlFile)
    75  	if err != nil {
    76  		exitWithError(fmt.Errorf("error opening %s test file: %v", testYamlFile, err))
    77  	}
    78  
    79  	glog.V(1).Info(fmt.Sprintf("Using test file: %s\n", testYamlFile))
    80  
    81  	// Get the viper config for this section of tests
    82  	typeConf := viper.Sub(string(nodetype))
    83  	if typeConf == nil {
    84  		colorPrint(check2.FAIL, fmt.Sprintf("No config settings for %s\n", string(nodetype)))
    85  		os.Exit(1)
    86  	}
    87  
    88  	// Get the set of executables we need for this section of the tests
    89  	binmap, err := getBinaries(typeConf, nodetype)
    90  	// Checks that the executables we need for the section are running.
    91  	if err != nil {
    92  		glog.V(1).Info(fmt.Sprintf("failed to get a set of executables needed for tests: %v", err))
    93  	}
    94  
    95  	confmap := getFiles(typeConf, "config")
    96  	svcmap := getFiles(typeConf, "service")
    97  	kubeconfmap := getFiles(typeConf, "kubeconfig")
    98  	cafilemap := getFiles(typeConf, "ca")
    99  	datadirmap := getFiles(typeConf, "datadir")
   100  
   101  	// Variable substitutions. Replace all occurrences of variables in controls files.
   102  	s := string(in)
   103  	s, binSubs := makeSubstitutions(s, "bin", binmap)
   104  	s, _ = makeSubstitutions(s, "conf", confmap)
   105  	s, _ = makeSubstitutions(s, "svc", svcmap)
   106  	s, _ = makeSubstitutions(s, "kubeconfig", kubeconfmap)
   107  	s, _ = makeSubstitutions(s, "cafile", cafilemap)
   108  	s, _ = makeSubstitutions(s, "datadir", datadirmap)
   109  
   110  	controls, err := check2.NewControls(nodetype, []byte(s), detectedVersion)
   111  	if err != nil {
   112  		exitWithError(fmt.Errorf("error setting up %s controls: %v", nodetype, err))
   113  	}
   114  
   115  	runner := check2.NewRunner()
   116  	filter, err := NewRunFilter(filterOpts)
   117  	if err != nil {
   118  		exitWithError(fmt.Errorf("error setting up run filter: %v", err))
   119  	}
   120  
   121  	generateDefaultEnvAudit(controls, binSubs)
   122  
   123  	controls.RunChecks(runner, filter, parseSkipIds(skipIds))
   124  	controlsCollection = append(controlsCollection, controls)
   125  }
   126  
   127  func generateDefaultEnvAudit(controls *check2.Controls, binSubs []string) {
   128  	for _, group := range controls.Groups {
   129  		for _, checkItem := range group.Checks {
   130  			if checkItem.Tests != nil && !checkItem.DisableEnvTesting {
   131  				for _, test := range checkItem.Tests.TestItems {
   132  					if test.Env != "" && checkItem.AuditEnv == "" {
   133  						binPath := ""
   134  
   135  						if len(binSubs) == 1 {
   136  							binPath = binSubs[0]
   137  						} else {
   138  							glog.V(1).Infof("AuditEnv not explicit for check (%s), where bin path cannot be determined", checkItem.ID)
   139  						}
   140  
   141  						if test.Env != "" && checkItem.AuditEnv == "" {
   142  							checkItem.AuditEnv = fmt.Sprintf("cat \"/proc/$(/bin/ps -C %s -o pid= | tr -d ' ')/environ\" | tr '\\0' '\\n'", binPath)
   143  						}
   144  					}
   145  				}
   146  			}
   147  		}
   148  	}
   149  }
   150  
   151  func parseSkipIds(skipIds string) map[string]bool {
   152  	skipIdMap := make(map[string]bool, 0)
   153  	if skipIds != "" {
   154  		for _, id := range strings.Split(skipIds, ",") {
   155  			skipIdMap[strings.Trim(id, " ")] = true
   156  		}
   157  	}
   158  	return skipIdMap
   159  }
   160  
   161  // colorPrint outputs the state in a specific colour, along with a message string
   162  func colorPrint(state check2.State, s string) {
   163  	colors[state].Printf("[%s] ", state)
   164  	fmt.Printf("%s", s)
   165  }
   166  
   167  // prettyPrint outputs the results to stdout in human-readable format
   168  func prettyPrint(r *check2.Controls, summary check2.Summary) {
   169  	// Print check results.
   170  	if !noResults {
   171  		colorPrint(check2.INFO, fmt.Sprintf("%s %s\n", r.ID, r.Text))
   172  		for _, g := range r.Groups {
   173  			colorPrint(check2.INFO, fmt.Sprintf("%s %s\n", g.ID, g.Text))
   174  			for _, c := range g.Checks {
   175  				colorPrint(c.State, fmt.Sprintf("%s %s\n", c.ID, c.Text))
   176  
   177  				if includeTestOutput && c.State == check2.FAIL && len(c.ActualValue) > 0 {
   178  					printRawOutput(c.ActualValue)
   179  				}
   180  			}
   181  		}
   182  
   183  		fmt.Println()
   184  	}
   185  
   186  	// Print remediations.
   187  	if !noRemediations {
   188  		if summary.Fail > 0 || summary.Warn > 0 {
   189  			colors[check2.WARN].Printf("== Remediations %s ==\n", r.Type)
   190  			for _, g := range r.Groups {
   191  				for _, c := range g.Checks {
   192  					if c.State == check2.FAIL {
   193  						fmt.Printf("%s %s\n", c.ID, c.Remediation)
   194  					}
   195  					if c.State == check2.WARN {
   196  						// Print the error if test failed due to problem with the audit command
   197  						if c.Reason != "" && c.Type != "manual" {
   198  							fmt.Printf("%s audit test did not run: %s\n", c.ID, c.Reason)
   199  						} else {
   200  							fmt.Printf("%s %s\n", c.ID, c.Remediation)
   201  						}
   202  					}
   203  				}
   204  			}
   205  			fmt.Println()
   206  		}
   207  	}
   208  
   209  	// Print summary setting output color to highest severity.
   210  	if !noSummary {
   211  		printSummary(summary, string(r.Type))
   212  	}
   213  }
   214  
   215  func printSummary(summary check2.Summary, sectionName string) {
   216  	var res check2.State
   217  	if summary.Fail > 0 {
   218  		res = check2.FAIL
   219  	} else if summary.Warn > 0 {
   220  		res = check2.WARN
   221  	} else {
   222  		res = check2.PASS
   223  	}
   224  
   225  	colors[res].Printf("== Summary %s ==\n", sectionName)
   226  	fmt.Printf("%d checks PASS\n%d checks FAIL\n%d checks WARN\n%d checks INFO\n\n",
   227  		summary.Pass, summary.Fail, summary.Warn, summary.Info,
   228  	)
   229  }
   230  
   231  // loadConfig finds the correct config dir based on the kubernetes version,
   232  // merges any specific config.yaml file found with the main config
   233  // and returns the benchmark file to use.
   234  func loadConfig(nodetype check2.NodeType, benchmarkVersion string) string {
   235  	var file string
   236  	var err error
   237  
   238  	switch nodetype {
   239  	case check2.MASTER:
   240  		file = masterFile
   241  	case check2.NODE:
   242  		file = nodeFile
   243  	case check2.CONTROLPLANE:
   244  		file = controlplaneFile
   245  	case check2.ETCD:
   246  		file = etcdFile
   247  	case check2.POLICIES:
   248  		file = policiesFile
   249  	case check2.MANAGEDSERVICES:
   250  		file = managedservicesFile
   251  	}
   252  
   253  	path, err := getConfigFilePath(benchmarkVersion, file)
   254  	if err != nil {
   255  		exitWithError(fmt.Errorf("can't find %s controls file in %s: %v", nodetype, cfgDir, err))
   256  	}
   257  
   258  	// Merge version-specific config if any.
   259  	mergeConfig(path)
   260  
   261  	return filepath.Join(path, file)
   262  }
   263  
   264  func mergeConfig(path string) error {
   265  	viper.SetConfigFile(path + "/config.yaml")
   266  	err := viper.MergeInConfig()
   267  	if err != nil {
   268  		if os.IsNotExist(err) {
   269  			glog.V(2).Info(fmt.Sprintf("No version-specific config.yaml file in %s", path))
   270  		} else {
   271  			return fmt.Errorf("couldn't read config file %s: %v", path+"/config.yaml", err)
   272  		}
   273  	}
   274  
   275  	glog.V(1).Info(fmt.Sprintf("Using config file: %s\n", viper.ConfigFileUsed()))
   276  
   277  	return nil
   278  }
   279  
   280  func mapToBenchmarkVersion(kubeToBenchmarkMap map[string]string, kv string) (string, error) {
   281  	kvOriginal := kv
   282  	cisVersion, found := kubeToBenchmarkMap[kv]
   283  	glog.V(2).Info(fmt.Sprintf("mapToBenchmarkVersion for k8sVersion: %q cisVersion: %q found: %t\n", kv, cisVersion, found))
   284  	for !found && (kv != defaultKubeVersion && !isEmpty(kv)) {
   285  		kv = decrementVersion(kv)
   286  		cisVersion, found = kubeToBenchmarkMap[kv]
   287  		glog.V(2).Info(fmt.Sprintf("mapToBenchmarkVersion for k8sVersion: %q cisVersion: %q found: %t\n", kv, cisVersion, found))
   288  	}
   289  
   290  	if !found {
   291  		glog.V(1).Info(fmt.Sprintf("mapToBenchmarkVersion unable to find a match for: %q", kvOriginal))
   292  		glog.V(3).Info(fmt.Sprintf("mapToBenchmarkVersion kubeToBenchmarkMap: %#v", kubeToBenchmarkMap))
   293  		return "", fmt.Errorf("unable to find a matching Benchmark Version match for kubernetes version: %s", kvOriginal)
   294  	}
   295  
   296  	return cisVersion, nil
   297  }
   298  
   299  func loadVersionMapping(v *viper.Viper) (map[string]string, error) {
   300  	kubeToBenchmarkMap := v.GetStringMapString("version_mapping")
   301  	if kubeToBenchmarkMap == nil || (len(kubeToBenchmarkMap) == 0) {
   302  		return nil, fmt.Errorf("config file is missing 'version_mapping' section")
   303  	}
   304  
   305  	return kubeToBenchmarkMap, nil
   306  }
   307  
   308  func loadTargetMapping(v *viper.Viper) (map[string][]string, error) {
   309  	benchmarkVersionToTargetsMap := v.GetStringMapStringSlice("target_mapping")
   310  	if len(benchmarkVersionToTargetsMap) == 0 {
   311  		return nil, fmt.Errorf("config file is missing 'target_mapping' section")
   312  	}
   313  
   314  	return benchmarkVersionToTargetsMap, nil
   315  }
   316  
   317  func getBenchmarkVersion(kubeVersion, benchmarkVersion string, platform Platform, v *viper.Viper) (bv string, err error) {
   318  	detecetedKubeVersion = "none"
   319  	if !isEmpty(kubeVersion) && !isEmpty(benchmarkVersion) {
   320  		return "", fmt.Errorf("It is an error to specify both --version and --benchmark flags")
   321  	}
   322  	if isEmpty(benchmarkVersion) && isEmpty(kubeVersion) && !isEmpty(platform.Name) {
   323  		benchmarkVersion = getPlatformBenchmarkVersion(platform)
   324  		if !isEmpty(benchmarkVersion) {
   325  			detecetedKubeVersion = benchmarkVersion
   326  		}
   327  	}
   328  
   329  	if isEmpty(benchmarkVersion) {
   330  		if isEmpty(kubeVersion) {
   331  			kv, err := getKubeVersion()
   332  			if err != nil {
   333  				return "", fmt.Errorf("Version check failed: %s\nAlternatively, you can specify the version with --version", err)
   334  			}
   335  			kubeVersion = kv.BaseVersion()
   336  			detecetedKubeVersion = kubeVersion
   337  		}
   338  
   339  		kubeToBenchmarkMap, err := loadVersionMapping(v)
   340  		if err != nil {
   341  			return "", err
   342  		}
   343  
   344  		benchmarkVersion, err = mapToBenchmarkVersion(kubeToBenchmarkMap, kubeVersion)
   345  		if err != nil {
   346  			return "", err
   347  		}
   348  
   349  		glog.V(2).Info(fmt.Sprintf("Mapped Kubernetes version: %s to Benchmark version: %s", kubeVersion, benchmarkVersion))
   350  	}
   351  
   352  	glog.V(1).Info(fmt.Sprintf("Kubernetes version: %q to Benchmark version: %q", kubeVersion, benchmarkVersion))
   353  	return benchmarkVersion, nil
   354  }
   355  
   356  // isMaster verify if master components are running on the node.
   357  func isMaster() bool {
   358  	return isThisNodeRunning(check2.MASTER)
   359  }
   360  
   361  // isEtcd verify if etcd components are running on the node.
   362  func isEtcd() bool {
   363  	return isThisNodeRunning(check2.ETCD)
   364  }
   365  
   366  func isThisNodeRunning(nodeType check2.NodeType) bool {
   367  	glog.V(3).Infof("Checking if the current node is running %s components", nodeType)
   368  	nodeTypeConf := viper.Sub(string(nodeType))
   369  	if nodeTypeConf == nil {
   370  		glog.V(2).Infof("No config for %s components found", nodeType)
   371  		return false
   372  	}
   373  
   374  	components, err := getBinariesFunc(nodeTypeConf, nodeType)
   375  	if err != nil {
   376  		glog.V(2).Infof("Failed to find %s binaries: %v", nodeType, err)
   377  		return false
   378  	}
   379  	if len(components) == 0 {
   380  		glog.V(2).Infof("No %s binaries specified", nodeType)
   381  		return false
   382  	}
   383  
   384  	glog.V(2).Infof("Node is running %s components", nodeType)
   385  	return true
   386  }
   387  
   388  func exitCodeSelection(controlsCollection []*check2.Controls) int {
   389  	for _, control := range controlsCollection {
   390  		if control.Fail > 0 {
   391  			return exitCode
   392  		}
   393  	}
   394  
   395  	return 0
   396  }
   397  
   398  func writeOutput(controlsCollection []*check2.Controls) {
   399  	sort.Slice(controlsCollection, func(i, j int) bool {
   400  		iid, _ := strconv.Atoi(controlsCollection[i].ID)
   401  		jid, _ := strconv.Atoi(controlsCollection[j].ID)
   402  		return iid < jid
   403  	})
   404  	if junitFmt {
   405  		writeJunitOutput(controlsCollection)
   406  		return
   407  	}
   408  	if jsonFmt {
   409  		writeJSONOutput(controlsCollection)
   410  		return
   411  	}
   412  	writeStdoutOutput(controlsCollection)
   413  }
   414  
   415  func writeJSONOutput(controlsCollection []*check2.Controls) {
   416  	var out []byte
   417  	var err error
   418  	if !noTotals {
   419  		var totals check2.OverallControls
   420  		totals.Controls = controlsCollection
   421  		totals.Totals = getSummaryTotals(controlsCollection)
   422  		out, err = json.Marshal(totals)
   423  	} else {
   424  		out, err = json.Marshal(controlsCollection)
   425  	}
   426  	if err != nil {
   427  		exitWithError(fmt.Errorf("failed to output in JSON format: %v", err))
   428  	}
   429  	printOutput(string(out), outputFile)
   430  }
   431  
   432  func writeJunitOutput(controlsCollection []*check2.Controls) {
   433  	// QuickFix for issue https://github.com/aquasecurity/kube-bench/issues/883
   434  	// Should consider to deprecate of switch to using Junit template
   435  	prefix := "<testsuites>\n"
   436  	suffix := "\n</testsuites>"
   437  	var outputAllControls []byte
   438  	for _, controls := range controlsCollection {
   439  		tempOut, err := controls.JUnit()
   440  		outputAllControls = append(outputAllControls[:], tempOut[:]...)
   441  		if err != nil {
   442  			exitWithError(fmt.Errorf("failed to output in JUnit format: %v", err))
   443  		}
   444  	}
   445  	printOutput(prefix+string(outputAllControls)+suffix, outputFile)
   446  }
   447  
   448  func writeStdoutOutput(controlsCollection []*check2.Controls) {
   449  	for _, controls := range controlsCollection {
   450  		summary := controls.Summary
   451  		prettyPrint(controls, summary)
   452  	}
   453  	if !noTotals {
   454  		printSummary(getSummaryTotals(controlsCollection), "total")
   455  	}
   456  }
   457  
   458  func getSummaryTotals(controlsCollection []*check2.Controls) check2.Summary {
   459  	var totalSummary check2.Summary
   460  	for _, controls := range controlsCollection {
   461  		summary := controls.Summary
   462  		totalSummary.Fail = totalSummary.Fail + summary.Fail
   463  		totalSummary.Warn = totalSummary.Warn + summary.Warn
   464  		totalSummary.Pass = totalSummary.Pass + summary.Pass
   465  		totalSummary.Info = totalSummary.Info + summary.Info
   466  	}
   467  	return totalSummary
   468  }
   469  
   470  func printRawOutput(output string) {
   471  	for _, row := range strings.Split(output, "\n") {
   472  		fmt.Println(fmt.Sprintf("\t %s", row))
   473  	}
   474  }
   475  
   476  func writeOutputToFile(output string, outputFile string) error {
   477  	file, err := os.Create(outputFile)
   478  	if err != nil {
   479  		return err
   480  	}
   481  	defer file.Close()
   482  
   483  	w := bufio.NewWriter(file)
   484  	fmt.Fprintln(w, output)
   485  	return w.Flush()
   486  }
   487  
   488  func printOutput(output string, outputFile string) {
   489  	if outputFile == "" {
   490  		fmt.Println(output)
   491  	} else {
   492  		err := writeOutputToFile(output, outputFile)
   493  		if err != nil {
   494  			exitWithError(fmt.Errorf("Failed to write to output file %s: %v", outputFile, err))
   495  		}
   496  	}
   497  }
   498  
   499  // validTargets helps determine if the targets
   500  // are legitimate for the benchmarkVersion.
   501  func validTargets(benchmarkVersion string, targets []string, v *viper.Viper) (bool, error) {
   502  	benchmarkVersionToTargetsMap, err := loadTargetMapping(v)
   503  	if err != nil {
   504  		return false, err
   505  	}
   506  	providedTargets, found := benchmarkVersionToTargetsMap[benchmarkVersion]
   507  	if !found {
   508  		return false, fmt.Errorf("No targets configured for %s", benchmarkVersion)
   509  	}
   510  
   511  	for _, pt := range targets {
   512  		f := false
   513  		for _, t := range providedTargets {
   514  			if pt == strings.ToLower(t) {
   515  				f = true
   516  				break
   517  			}
   518  		}
   519  
   520  		if !f {
   521  			return false, nil
   522  		}
   523  	}
   524  
   525  	return true, nil
   526  }