1
0
mirror of https://github.com/aquasecurity/kube-bench.git synced 2024-12-24 15:38:06 +00:00
kube-bench/check/test.go
Liz Rice 6452df7c7f
Expected result pattern not always shows (#784)
* Add expectedResultPattern to invalid test

when testing and try convert to numeric we didn't set expectedResultPattern value.

* check for auditconfig before using it

The current state is that when ever audit output is not what we search for we check for auditConfig output which is sometime empty and therefore create empty expected result as described in #694

* Fix issue about expectedResultPattern

expectedResultPattern not always shown and wasn't accurate enough 
Issue #705

* Add tests for ExpectedResult and fixes

Add tests for ExpectedResult with the new output and the verify that the fix is working

* Add missing flags

In some cases not having audit or audit_config flag would fail the test.
So added just a simple commands like echo something to solve this issue 
Also add bitmask checks

* Add example IAM policy

* Pass RotateKubeletServerCertificate related checks if it's not found (#767)

* Allow for environment variables to be checked in tests (#755)

* Initial commit for checking environment variables for etcd

* Revert config changes

* Remove redundant struct data

* Fix issues with failing tests

* Initial changes based on code review

* Add option to disable envTesting + Update docs

* Initial tests

* Finished testing

* Fix broken tests

* Add a total summary and always show all tests. (#759)

Whether the total summary is shown can be specified with an option.

Fixes #528

Signed-off-by: Christian Zunker <christian.zunker@codecentric.cloud>

* Update Readme.md file with link to Contribution guide (#754)

* Update License with the year and the owner name

Please add this to make your license agreement strong

* Updated Readme.md file with license and proper documentation links

I have added a proper license agreement to the documentation. Also shortened the links to the issues so that it does not break in any on the forks.

* Update LICENSE

* Update README.md

* Update README.md

* Remove erroneous license info

Co-authored-by: Liz Rice <liz@lizrice.com>

* Support auto-detect platform when running on EKS or GKE (#683)

* Support auto-detect platform when running on EKS or GKE

* Change to get platform name from `kubectl version`

* fix regexp and add test

* Update Server Version match for EKS

* try to get version info from api sever at first

* Change expected expectedResultPattern

Now expectedResultPattern is more verbose

* Update ops tests

* Fix unit tests

* Fix bitmask output syntax

* Changes to be committed:
	modified:   check/check.go
	modified:   check/test.go
	modified:   check/test_test.go
fix unit testing and test.go to resolve conflicts.

* Change found to flagFound

* add missing }

* change found to flag found

Co-authored-by: yoavrotems <yoavrotems97@gmail.com>
2020-12-24 16:38:22 +02:00

430 lines
10 KiB
Go

// Copyright © 2017 Aqua Security Software Ltd. <info@aquasec.com>
//
// 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 check
import (
"bytes"
"encoding/json"
"fmt"
"os"
"regexp"
"strconv"
"strings"
"github.com/golang/glog"
yaml "gopkg.in/yaml.v2"
"k8s.io/client-go/util/jsonpath"
)
// test:
// flag: OPTION
// set: (true|false)
// compare:
// op: (eq|gt|gte|lt|lte|has)
// value: val
type binOp string
const (
and binOp = "and"
or = "or"
defaultArraySeparator = ","
)
type tests struct {
TestItems []*testItem `yaml:"test_items"`
BinOp binOp `yaml:"bin_op"`
}
type AuditUsed string
const (
AuditCommand AuditUsed = "auditCommand"
AuditConfig AuditUsed = "auditConfig"
AuditEnv AuditUsed = "auditEnv"
)
type testItem struct {
Flag string
Env string
Path string
Output string
Value string
Set bool
Compare compare
isMultipleOutput bool
auditUsed AuditUsed
}
type envTestItem testItem
type pathTestItem testItem
type flagTestItem testItem
type compare struct {
Op string
Value string
}
type testOutput struct {
testResult bool
flagFound bool
actualResult string
ExpectedResult string
}
func failTestItem(s string) *testOutput {
return &testOutput{testResult: false, actualResult: s}
}
func (t testItem) value() string {
if t.auditUsed == AuditConfig {
return t.Path
}
if t.auditUsed == AuditEnv {
return t.Env
}
return t.Flag
}
func (t testItem) findValue(s string) (match bool, value string, err error) {
if t.auditUsed == AuditEnv {
et := envTestItem(t)
return et.findValue(s)
}
if t.auditUsed == AuditConfig {
pt := pathTestItem(t)
return pt.findValue(s)
}
ft := flagTestItem(t)
return ft.findValue(s)
}
func (t flagTestItem) findValue(s string) (match bool, value string, err error) {
if s == "" || t.Flag == "" {
return
}
match = strings.Contains(s, t.Flag)
if match {
// Expects flags in the form;
// --flag=somevalue
// flag: somevalue
// --flag
// somevalue
pttn := `(` + t.Flag + `)(=|: *)*([^\s]*) *`
flagRe := regexp.MustCompile(pttn)
vals := flagRe.FindStringSubmatch(s)
if len(vals) > 0 {
if vals[3] != "" {
value = vals[3]
} else {
// --bool-flag
if strings.HasPrefix(t.Flag, "--") {
value = "true"
} else {
value = vals[1]
}
}
} else {
err = fmt.Errorf("invalid flag in testItem definition: %s", s)
}
}
glog.V(3).Infof("In flagTestItem.findValue %s, match %v, s %s, t.Flag %s", value, match, s, t.Flag)
return match, value, err
}
func (t pathTestItem) findValue(s string) (match bool, value string, err error) {
var jsonInterface interface{}
err = unmarshal(s, &jsonInterface)
if err != nil {
return false, "", fmt.Errorf("failed to load YAML or JSON from input \"%s\": %v", s, err)
}
value, err = executeJSONPath(t.Path, &jsonInterface)
if err != nil {
return false, "", fmt.Errorf("unable to parse path expression \"%s\": %v", t.Path, err)
}
glog.V(3).Infof("In pathTestItem.findValue %s", value)
match = value != ""
return match, value, err
}
func (t envTestItem) findValue(s string) (match bool, value string, err error) {
if s != "" && t.Env != "" {
r, _ := regexp.Compile(fmt.Sprintf("%s=.*(?:$|\\n)", t.Env))
out := r.FindString(s)
out = strings.Replace(out, "\n", "", 1)
out = strings.Replace(out, fmt.Sprintf("%s=", t.Env), "", 1)
if len(out) > 0 {
match = true
value = out
} else {
match = false
value = ""
}
}
return match, value, nil
}
func (t testItem) execute(s string) *testOutput {
result := &testOutput{}
s = strings.TrimRight(s, " \n")
// If the test has output that should be evaluated for each row
var output []string
if t.isMultipleOutput {
output = strings.Split(s, "\n")
} else {
output = []string{s}
}
for _, op := range output {
result = t.evaluate(op)
// If the test failed for the current row, no need to keep testing for this output
if !result.testResult {
break
}
}
result.actualResult = s
return result
}
func (t testItem) evaluate(s string) *testOutput {
result := &testOutput{}
match, value, err := t.findValue(s)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
return failTestItem(err.Error())
}
if t.Set {
if match && t.Compare.Op != "" {
result.ExpectedResult, result.testResult = compareOp(t.Compare.Op, value, t.Compare.Value, t.value())
} else {
result.ExpectedResult = fmt.Sprintf("'%s' is present", t.value())
result.testResult = match
}
} else {
result.ExpectedResult = fmt.Sprintf("'%s' is not present", t.value())
result.testResult = !match
}
result.flagFound = match
glog.V(3).Info(fmt.Sprintf("found %v", result.flagFound))
return result
}
func compareOp(tCompareOp string, flagVal string, tCompareValue string, flagName string) (string, bool) {
expectedResultPattern := ""
testResult := false
switch tCompareOp {
case "eq":
expectedResultPattern = "'%s' is equal to '%s'"
value := strings.ToLower(flagVal)
// Do case insensitive comparaison for booleans ...
if value == "false" || value == "true" {
testResult = value == tCompareValue
} else {
testResult = flagVal == tCompareValue
}
case "noteq":
expectedResultPattern = "'%s' is not equal to '%s'"
value := strings.ToLower(flagVal)
// Do case insensitive comparaison for booleans ...
if value == "false" || value == "true" {
testResult = !(value == tCompareValue)
} else {
testResult = !(flagVal == tCompareValue)
}
case "gt", "gte", "lt", "lte":
a, b, err := toNumeric(flagVal, tCompareValue)
if err != nil {
expectedResultPattern = "Invalid Number(s) used for comparison: '%s' '%s'"
glog.V(1).Infof(fmt.Sprintf("Not numeric value - flag: %q - compareValue: %q %v\n", flagVal, tCompareValue, err))
return fmt.Sprintf(expectedResultPattern, flagVal, tCompareValue), false
}
switch tCompareOp {
case "gt":
expectedResultPattern = "'%s' is greater than %s"
testResult = a > b
case "gte":
expectedResultPattern = "'%s' is greater or equal to %s"
testResult = a >= b
case "lt":
expectedResultPattern = "'%s' is lower than %s"
testResult = a < b
case "lte":
expectedResultPattern = "'%s' is lower or equal to %s"
testResult = a <= b
}
case "has":
expectedResultPattern = "'%s' has '%s'"
testResult = strings.Contains(flagVal, tCompareValue)
case "nothave":
expectedResultPattern = "'%s' does not have '%s'"
testResult = !strings.Contains(flagVal, tCompareValue)
case "regex":
expectedResultPattern = "'%s' matched by regex expression '%s'"
opRe := regexp.MustCompile(tCompareValue)
testResult = opRe.MatchString(flagVal)
case "valid_elements":
expectedResultPattern = "'%s' contains valid elements from '%s'"
s := splitAndRemoveLastSeparator(flagVal, defaultArraySeparator)
target := splitAndRemoveLastSeparator(tCompareValue, defaultArraySeparator)
testResult = allElementsValid(s, target)
case "bitmask":
expectedResultPattern = "%s has permissions " + flagVal + ", expected %s or more restrictive"
requested, err := strconv.ParseInt(flagVal, 8, 64)
if err != nil {
glog.V(1).Infof(fmt.Sprintf("Not numeric value - flag: %q - compareValue: %q %v\n", flagVal, tCompareValue, err))
return fmt.Sprintf("Not numeric value - flag: %s", flagVal), false
}
max, err := strconv.ParseInt(tCompareValue, 8, 64)
if err != nil {
glog.V(1).Infof(fmt.Sprintf("Not numeric value - flag: %q - compareValue: %q %v\n", flagVal, tCompareValue, err))
return fmt.Sprintf("Not numeric value - flag: %s", tCompareValue), false
}
testResult = (max & requested) == requested
}
if expectedResultPattern == "" {
return expectedResultPattern, testResult
}
return fmt.Sprintf(expectedResultPattern, flagName, tCompareValue), testResult
}
func unmarshal(s string, jsonInterface *interface{}) error {
data := []byte(s)
err := json.Unmarshal(data, jsonInterface)
if err != nil {
err := yaml.Unmarshal(data, jsonInterface)
if err != nil {
return err
}
}
return nil
}
func executeJSONPath(path string, jsonInterface interface{}) (string, error) {
j := jsonpath.New("jsonpath")
j.AllowMissingKeys(true)
err := j.Parse(path)
if err != nil {
return "", err
}
buf := new(bytes.Buffer)
err = j.Execute(buf, jsonInterface)
if err != nil {
return "", err
}
jsonpathResult := buf.String()
return jsonpathResult, nil
}
func allElementsValid(s, t []string) bool {
sourceEmpty := len(s) == 0
targetEmpty := len(t) == 0
if sourceEmpty && targetEmpty {
return true
}
// XOR comparison -
// if either value is empty and the other is not empty,
// not all elements are valid
if (sourceEmpty || targetEmpty) && !(sourceEmpty && targetEmpty) {
return false
}
for _, sv := range s {
found := false
for _, tv := range t {
if sv == tv {
found = true
break
}
}
if !found {
return false
}
}
return true
}
func splitAndRemoveLastSeparator(s, sep string) []string {
cleanS := strings.TrimRight(strings.TrimSpace(s), sep)
if len(cleanS) == 0 {
return []string{}
}
ts := strings.Split(cleanS, sep)
for i := range ts {
ts[i] = strings.TrimSpace(ts[i])
}
return ts
}
func toNumeric(a, b string) (c, d int, err error) {
c, err = strconv.Atoi(strings.TrimSpace(a))
if err != nil {
return -1, -1, fmt.Errorf("toNumeric - error converting %s: %s", a, err)
}
d, err = strconv.Atoi(strings.TrimSpace(b))
if err != nil {
return -1, -1, fmt.Errorf("toNumeric - error converting %s: %s", b, err)
}
return c, d, nil
}
func (t *testItem) UnmarshalYAML(unmarshal func(interface{}) error) error {
type buildTest testItem
// Make Set parameter to be true by default.
newTestItem := buildTest{Set: true}
err := unmarshal(&newTestItem)
if err != nil {
return err
}
*t = testItem(newTestItem)
return nil
}