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 }