github.com/khulnasoft-lab/kube-bench@v0.2.1-0.20240330183753-9df52345ae58/check/test.go (about)

     1  // Copyright © 2017 Khulnasoft Security Software Ltd. <info@khulnasoft.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 check
    16  
    17  import (
    18  	"bytes"
    19  	"encoding/json"
    20  	"fmt"
    21  	"os"
    22  	"regexp"
    23  	"strconv"
    24  	"strings"
    25  
    26  	"github.com/golang/glog"
    27  	"gopkg.in/yaml.v2"
    28  	"k8s.io/client-go/util/jsonpath"
    29  )
    30  
    31  // test:
    32  // flag: OPTION
    33  // set: (true|false)
    34  // compare:
    35  //   op: (eq|gt|gte|lt|lte|has)
    36  //   value: val
    37  
    38  type binOp string
    39  
    40  const (
    41  	and                   binOp = "and"
    42  	or                          = "or"
    43  	defaultArraySeparator       = ","
    44  )
    45  
    46  type tests struct {
    47  	TestItems []*testItem `yaml:"test_items"`
    48  	BinOp     binOp       `yaml:"bin_op"`
    49  }
    50  
    51  type AuditUsed string
    52  
    53  const (
    54  	AuditCommand AuditUsed = "auditCommand"
    55  	AuditConfig  AuditUsed = "auditConfig"
    56  	AuditEnv     AuditUsed = "auditEnv"
    57  )
    58  
    59  type testItem struct {
    60  	Flag             string
    61  	Env              string
    62  	Path             string
    63  	Output           string
    64  	Value            string
    65  	Set              bool
    66  	Compare          compare
    67  	isMultipleOutput bool
    68  	auditUsed        AuditUsed
    69  }
    70  
    71  type (
    72  	envTestItem  testItem
    73  	pathTestItem testItem
    74  	flagTestItem testItem
    75  )
    76  
    77  type compare struct {
    78  	Op    string
    79  	Value string
    80  }
    81  
    82  type testOutput struct {
    83  	testResult     bool
    84  	flagFound      bool
    85  	actualResult   string
    86  	ExpectedResult string
    87  }
    88  
    89  func failTestItem(s string) *testOutput {
    90  	return &testOutput{testResult: false, actualResult: s}
    91  }
    92  
    93  func (t testItem) value() string {
    94  	if t.auditUsed == AuditConfig {
    95  		return t.Path
    96  	}
    97  
    98  	if t.auditUsed == AuditEnv {
    99  		return t.Env
   100  	}
   101  
   102  	return t.Flag
   103  }
   104  
   105  func (t testItem) findValue(s string) (match bool, value string, err error) {
   106  	if t.auditUsed == AuditEnv {
   107  		et := envTestItem(t)
   108  		return et.findValue(s)
   109  	}
   110  
   111  	if t.auditUsed == AuditConfig {
   112  		pt := pathTestItem(t)
   113  		return pt.findValue(s)
   114  	}
   115  
   116  	ft := flagTestItem(t)
   117  	return ft.findValue(s)
   118  }
   119  
   120  func (t flagTestItem) findValue(s string) (match bool, value string, err error) {
   121  	if s == "" || t.Flag == "" {
   122  		return
   123  	}
   124  	match = strings.Contains(s, t.Flag)
   125  	if match {
   126  		// Expects flags in the form;
   127  		// --flag=somevalue
   128  		// flag: somevalue
   129  		// --flag
   130  		// somevalue
   131  		// DOESN'T COVER - use pathTestItem implementation of findValue() for this
   132  		// flag:
   133  		//	 - wehbook
   134  		pttn := `(` + t.Flag + `)(=|: *)*([^\s]*) *`
   135  		flagRe := regexp.MustCompile(pttn)
   136  		vals := flagRe.FindStringSubmatch(s)
   137  
   138  		if len(vals) > 0 {
   139  			if vals[3] != "" {
   140  				value = vals[3]
   141  			} else {
   142  				// --bool-flag
   143  				if strings.HasPrefix(t.Flag, "--") {
   144  					value = "true"
   145  				} else {
   146  					value = vals[1]
   147  				}
   148  			}
   149  		} else {
   150  			err = fmt.Errorf("invalid flag in testItem definition: %s", s)
   151  		}
   152  	}
   153  	glog.V(3).Infof("In flagTestItem.findValue %s", value)
   154  
   155  	return match, value, err
   156  }
   157  
   158  func (t pathTestItem) findValue(s string) (match bool, value string, err error) {
   159  	var jsonInterface interface{}
   160  
   161  	err = unmarshal(s, &jsonInterface)
   162  	if err != nil {
   163  		return false, "", fmt.Errorf("failed to load YAML or JSON from input \"%s\": %v", s, err)
   164  	}
   165  
   166  	value, err = executeJSONPath(t.Path, &jsonInterface)
   167  	if err != nil {
   168  		return false, "", fmt.Errorf("unable to parse path expression \"%s\": %v", t.Path, err)
   169  	}
   170  
   171  	glog.V(3).Infof("In pathTestItem.findValue %s", value)
   172  	match = value != ""
   173  	return match, value, err
   174  }
   175  
   176  func (t envTestItem) findValue(s string) (match bool, value string, err error) {
   177  	if s != "" && t.Env != "" {
   178  		r, _ := regexp.Compile(fmt.Sprintf("%s=.*(?:$|\\n)", t.Env))
   179  		out := r.FindString(s)
   180  		out = strings.Replace(out, "\n", "", 1)
   181  		out = strings.Replace(out, fmt.Sprintf("%s=", t.Env), "", 1)
   182  
   183  		if len(out) > 0 {
   184  			match = true
   185  			value = out
   186  		} else {
   187  			match = false
   188  			value = ""
   189  		}
   190  	}
   191  	glog.V(3).Infof("In envTestItem.findValue %s", value)
   192  	return match, value, nil
   193  }
   194  
   195  func (t testItem) execute(s string) *testOutput {
   196  	result := &testOutput{}
   197  	s = strings.TrimRight(s, " \n")
   198  
   199  	// If the test has output that should be evaluated for each row
   200  	var output []string
   201  	if t.isMultipleOutput {
   202  		output = strings.Split(s, "\n")
   203  	} else {
   204  		output = []string{s}
   205  	}
   206  
   207  	for _, op := range output {
   208  		result = t.evaluate(op)
   209  		// If the test failed for the current row, no need to keep testing for this output
   210  		if !result.testResult {
   211  			break
   212  		}
   213  	}
   214  
   215  	result.actualResult = s
   216  	return result
   217  }
   218  
   219  func (t testItem) evaluate(s string) *testOutput {
   220  	result := &testOutput{}
   221  
   222  	match, value, err := t.findValue(s)
   223  	if err != nil {
   224  		fmt.Fprintf(os.Stderr, err.Error())
   225  		return failTestItem(err.Error())
   226  	}
   227  
   228  	if t.Set {
   229  		if match && t.Compare.Op != "" {
   230  			result.ExpectedResult, result.testResult = compareOp(t.Compare.Op, value, t.Compare.Value, t.value())
   231  		} else {
   232  			result.ExpectedResult = fmt.Sprintf("'%s' is present", t.value())
   233  			result.testResult = match
   234  		}
   235  	} else {
   236  		result.ExpectedResult = fmt.Sprintf("'%s' is not present", t.value())
   237  		result.testResult = !match
   238  	}
   239  
   240  	result.flagFound = match
   241  	isExist := "exists"
   242  	if !result.flagFound {
   243  		isExist = "does not exist"
   244  	}
   245  	switch t.auditUsed {
   246  	case AuditCommand:
   247  		glog.V(3).Infof("Flag '%s' %s", t.Flag, isExist)
   248  	case AuditConfig:
   249  		glog.V(3).Infof("Path '%s' %s", t.Path, isExist)
   250  	case AuditEnv:
   251  		glog.V(3).Infof("Env '%s' %s", t.Env, isExist)
   252  	default:
   253  		glog.V(3).Infof("Error with identify audit used %s", t.auditUsed)
   254  	}
   255  
   256  	return result
   257  }
   258  
   259  func compareOp(tCompareOp string, flagVal string, tCompareValue string, flagName string) (string, bool) {
   260  	expectedResultPattern := ""
   261  	testResult := false
   262  
   263  	switch tCompareOp {
   264  	case "eq":
   265  		expectedResultPattern = "'%s' is equal to '%s'"
   266  		value := strings.ToLower(flagVal)
   267  		// Do case insensitive comparaison for booleans ...
   268  		if value == "false" || value == "true" {
   269  			testResult = value == tCompareValue
   270  		} else {
   271  			testResult = flagVal == tCompareValue
   272  		}
   273  
   274  	case "noteq":
   275  		expectedResultPattern = "'%s' is not equal to '%s'"
   276  		value := strings.ToLower(flagVal)
   277  		// Do case insensitive comparaison for booleans ...
   278  		if value == "false" || value == "true" {
   279  			testResult = !(value == tCompareValue)
   280  		} else {
   281  			testResult = !(flagVal == tCompareValue)
   282  		}
   283  
   284  	case "gt", "gte", "lt", "lte":
   285  		a, b, err := toNumeric(flagVal, tCompareValue)
   286  		if err != nil {
   287  			expectedResultPattern = "Invalid Number(s) used for comparison: '%s' '%s'"
   288  			glog.V(1).Infof(fmt.Sprintf("Not numeric value - flag: %q - compareValue: %q %v\n", flagVal, tCompareValue, err))
   289  			return fmt.Sprintf(expectedResultPattern, flagVal, tCompareValue), false
   290  		}
   291  		switch tCompareOp {
   292  		case "gt":
   293  			expectedResultPattern = "'%s' is greater than %s"
   294  			testResult = a > b
   295  
   296  		case "gte":
   297  			expectedResultPattern = "'%s' is greater or equal to %s"
   298  			testResult = a >= b
   299  
   300  		case "lt":
   301  			expectedResultPattern = "'%s' is lower than %s"
   302  			testResult = a < b
   303  
   304  		case "lte":
   305  			expectedResultPattern = "'%s' is lower or equal to %s"
   306  			testResult = a <= b
   307  		}
   308  
   309  	case "has":
   310  		expectedResultPattern = "'%s' has '%s'"
   311  		testResult = strings.Contains(flagVal, tCompareValue)
   312  
   313  	case "nothave":
   314  		expectedResultPattern = "'%s' does not have '%s'"
   315  		testResult = !strings.Contains(flagVal, tCompareValue)
   316  
   317  	case "regex":
   318  		expectedResultPattern = "'%s' matched by regex expression '%s'"
   319  		opRe := regexp.MustCompile(tCompareValue)
   320  		testResult = opRe.MatchString(flagVal)
   321  
   322  	case "valid_elements":
   323  		expectedResultPattern = "'%s' contains valid elements from '%s'"
   324  		s := splitAndRemoveLastSeparator(flagVal, defaultArraySeparator)
   325  		target := splitAndRemoveLastSeparator(tCompareValue, defaultArraySeparator)
   326  		testResult = allElementsValid(s, target)
   327  
   328  	case "bitmask":
   329  		expectedResultPattern = "%s has permissions " + flagVal + ", expected %s or more restrictive"
   330  		requested, err := strconv.ParseInt(flagVal, 8, 64)
   331  		if err != nil {
   332  			glog.V(1).Infof(fmt.Sprintf("Not numeric value - flag: %q - compareValue: %q %v\n", flagVal, tCompareValue, err))
   333  			return fmt.Sprintf("Not numeric value - flag: %s", flagVal), false
   334  		}
   335  		max, err := strconv.ParseInt(tCompareValue, 8, 64)
   336  		if err != nil {
   337  			glog.V(1).Infof(fmt.Sprintf("Not numeric value - flag: %q - compareValue: %q %v\n", flagVal, tCompareValue, err))
   338  			return fmt.Sprintf("Not numeric value - flag: %s", tCompareValue), false
   339  		}
   340  		testResult = (max & requested) == requested
   341  	}
   342  	if expectedResultPattern == "" {
   343  		return expectedResultPattern, testResult
   344  	}
   345  
   346  	return fmt.Sprintf(expectedResultPattern, flagName, tCompareValue), testResult
   347  }
   348  
   349  func unmarshal(s string, jsonInterface *interface{}) error {
   350  	data := []byte(s)
   351  	err := json.Unmarshal(data, jsonInterface)
   352  	if err != nil {
   353  		err := yaml.Unmarshal(data, jsonInterface)
   354  		if err != nil {
   355  			return err
   356  		}
   357  	}
   358  	return nil
   359  }
   360  
   361  func executeJSONPath(path string, jsonInterface interface{}) (string, error) {
   362  	j := jsonpath.New("jsonpath")
   363  	j.AllowMissingKeys(true)
   364  	err := j.Parse(path)
   365  	if err != nil {
   366  		return "", err
   367  	}
   368  
   369  	buf := new(bytes.Buffer)
   370  	err = j.Execute(buf, jsonInterface)
   371  	if err != nil {
   372  		return "", err
   373  	}
   374  	jsonpathResult := buf.String()
   375  	return jsonpathResult, nil
   376  }
   377  
   378  func allElementsValid(s, t []string) bool {
   379  	sourceEmpty := len(s) == 0
   380  	targetEmpty := len(t) == 0
   381  
   382  	if sourceEmpty && targetEmpty {
   383  		return true
   384  	}
   385  
   386  	// XOR comparison -
   387  	//     if either value is empty and the other is not empty,
   388  	//     not all elements are valid
   389  	if (sourceEmpty || targetEmpty) && !(sourceEmpty && targetEmpty) {
   390  		return false
   391  	}
   392  
   393  	for _, sv := range s {
   394  		found := false
   395  		for _, tv := range t {
   396  			if sv == tv {
   397  				found = true
   398  				break
   399  			}
   400  		}
   401  		if !found {
   402  			return false
   403  		}
   404  	}
   405  	return true
   406  }
   407  
   408  func splitAndRemoveLastSeparator(s, sep string) []string {
   409  	cleanS := strings.TrimRight(strings.TrimSpace(s), sep)
   410  	if len(cleanS) == 0 {
   411  		return []string{}
   412  	}
   413  
   414  	ts := strings.Split(cleanS, sep)
   415  	for i := range ts {
   416  		ts[i] = strings.TrimSpace(ts[i])
   417  	}
   418  
   419  	return ts
   420  }
   421  
   422  func toNumeric(a, b string) (c, d int, err error) {
   423  	c, err = strconv.Atoi(strings.TrimSpace(a))
   424  	if err != nil {
   425  		return -1, -1, fmt.Errorf("toNumeric - error converting %s: %s", a, err)
   426  	}
   427  	d, err = strconv.Atoi(strings.TrimSpace(b))
   428  	if err != nil {
   429  		return -1, -1, fmt.Errorf("toNumeric - error converting %s: %s", b, err)
   430  	}
   431  
   432  	return c, d, nil
   433  }
   434  
   435  func (t *testItem) UnmarshalYAML(unmarshal func(interface{}) error) error {
   436  	type buildTest testItem
   437  
   438  	// Make Set parameter to be true by default.
   439  	newTestItem := buildTest{Set: true}
   440  	err := unmarshal(&newTestItem)
   441  	if err != nil {
   442  		return err
   443  	}
   444  	*t = testItem(newTestItem)
   445  	return nil
   446  }