You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
kube-bench/check/test.go

447 lines
11 KiB

// 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
pathTestItem testItem
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
// DOESN'T COVER - use pathTestItem implementation of findValue() for this
// flag:
// - wehbook
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", value)
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 = ""
}
}
glog.V(3).Infof("In envTestItem.findValue %s", 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
isExist := "exists"
if !result.flagFound {
isExist = "does not exist"
}
switch t.auditUsed {
case "auditCommand":
glog.V(3).Infof("Flag '%s' %s", t.Flag, isExist)
case "auditConfig":
glog.V(3).Infof("Path '%s' %s", t.Path, isExist)
case "auditEnv":
glog.V(3).Infof("Env '%s' %s", t.Env, isExist)
default:
glog.V(3).Infof("Error with identify audit used %s", t.auditUsed)
}
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
}