mirror of
https://github.com/aquasecurity/kube-bench.git
synced 2024-12-01 12:28:18 +00:00
fc72a8a620
In case of RKE, env error comes with exit status 1, so added OR codition to match with error text as well. resolve: #1364
314 lines
8.3 KiB
Go
314 lines
8.3 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"
|
|
"fmt"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"github.com/golang/glog"
|
|
)
|
|
|
|
// NodeType indicates the type of node (master, node).
|
|
type NodeType string
|
|
|
|
// State is the state of a control check.
|
|
type State string
|
|
|
|
const (
|
|
// PASS check passed.
|
|
PASS State = "PASS"
|
|
// FAIL check failed.
|
|
FAIL State = "FAIL"
|
|
// WARN could not carry out check.
|
|
WARN State = "WARN"
|
|
// INFO informational message
|
|
INFO State = "INFO"
|
|
|
|
// SKIP for when a check should be skipped.
|
|
SKIP = "skip"
|
|
|
|
// MASTER a master node
|
|
MASTER NodeType = "master"
|
|
// NODE a node
|
|
NODE NodeType = "node"
|
|
// FEDERATED a federated deployment.
|
|
FEDERATED NodeType = "federated"
|
|
|
|
// ETCD an etcd node
|
|
ETCD NodeType = "etcd"
|
|
// CONTROLPLANE a control plane node
|
|
CONTROLPLANE NodeType = "controlplane"
|
|
// POLICIES a node to run policies from
|
|
POLICIES NodeType = "policies"
|
|
// MANAGEDSERVICES a node to run managedservices from
|
|
MANAGEDSERVICES = "managedservices"
|
|
|
|
// MANUAL Check Type
|
|
MANUAL string = "manual"
|
|
)
|
|
|
|
// Check contains information about a recommendation in the
|
|
// CIS Kubernetes document.
|
|
type Check struct {
|
|
ID string `yaml:"id" json:"test_number"`
|
|
Text string `json:"test_desc"`
|
|
Audit string `json:"audit"`
|
|
AuditEnv string `yaml:"audit_env"`
|
|
AuditConfig string `yaml:"audit_config"`
|
|
Type string `json:"type"`
|
|
Tests *tests `json:"-"`
|
|
Set bool `json:"-"`
|
|
Remediation string `json:"remediation"`
|
|
TestInfo []string `json:"test_info"`
|
|
State `json:"status"`
|
|
ActualValue string `json:"actual_value"`
|
|
Scored bool `json:"scored"`
|
|
IsMultiple bool `yaml:"use_multiple_values"`
|
|
ExpectedResult string `json:"expected_result"`
|
|
Reason string `json:"reason,omitempty"`
|
|
AuditOutput string `json:"-"`
|
|
AuditEnvOutput string `json:"-"`
|
|
AuditConfigOutput string `json:"-"`
|
|
DisableEnvTesting bool `json:"-"`
|
|
}
|
|
|
|
// Runner wraps the basic Run method.
|
|
type Runner interface {
|
|
// Run runs a given check and returns the execution state.
|
|
Run(c *Check) State
|
|
}
|
|
|
|
// NewRunner constructs a default Runner.
|
|
func NewRunner() Runner {
|
|
return &defaultRunner{}
|
|
}
|
|
|
|
type defaultRunner struct{}
|
|
|
|
func (r *defaultRunner) Run(c *Check) State {
|
|
return c.run()
|
|
}
|
|
|
|
// Run executes the audit commands specified in a check and outputs
|
|
// the results.
|
|
func (c *Check) run() State {
|
|
glog.V(3).Infof("----- Running check %v -----", c.ID)
|
|
// Since this is an Scored check
|
|
// without tests return a 'WARN' to alert
|
|
// the user that this check needs attention
|
|
if c.Scored && strings.TrimSpace(c.Type) == "" && c.Tests == nil {
|
|
c.Reason = "There are no tests"
|
|
c.State = WARN
|
|
glog.V(3).Info(c.Reason)
|
|
return c.State
|
|
}
|
|
|
|
// If check type is skip, force result to INFO
|
|
if c.Type == SKIP {
|
|
c.Reason = "Test marked as skip"
|
|
c.State = INFO
|
|
glog.V(3).Info(c.Reason)
|
|
return c.State
|
|
}
|
|
|
|
// If check type is manual force result to WARN
|
|
if c.Type == MANUAL {
|
|
c.Reason = "Test marked as a manual test"
|
|
c.State = WARN
|
|
glog.V(3).Info(c.Reason)
|
|
return c.State
|
|
}
|
|
|
|
// If there aren't any tests defined this is a FAIL or WARN
|
|
if c.Tests == nil || len(c.Tests.TestItems) == 0 {
|
|
c.Reason = "No tests defined"
|
|
if c.Scored {
|
|
c.State = FAIL
|
|
} else {
|
|
c.State = WARN
|
|
}
|
|
glog.V(3).Info(c.Reason)
|
|
return c.State
|
|
}
|
|
|
|
// Command line parameters override the setting in the config file, so if we get a good result from the Audit command that's all we need to run
|
|
var finalOutput *testOutput
|
|
var lastCommand string
|
|
|
|
lastCommand, err := c.runAuditCommands()
|
|
if err == nil {
|
|
finalOutput, err = c.execute()
|
|
}
|
|
|
|
if finalOutput != nil {
|
|
if finalOutput.testResult {
|
|
c.State = PASS
|
|
} else {
|
|
if c.Scored {
|
|
c.State = FAIL
|
|
} else {
|
|
c.State = WARN
|
|
}
|
|
}
|
|
|
|
c.ActualValue = finalOutput.actualResult
|
|
c.ExpectedResult = finalOutput.ExpectedResult
|
|
}
|
|
|
|
if err != nil {
|
|
c.Reason = err.Error()
|
|
if c.Scored {
|
|
c.State = FAIL
|
|
} else {
|
|
c.State = WARN
|
|
}
|
|
glog.V(3).Info(c.Reason)
|
|
}
|
|
|
|
if finalOutput != nil {
|
|
glog.V(3).Infof("Command: %q TestResult: %t State: %q \n", lastCommand, finalOutput.testResult, c.State)
|
|
} else {
|
|
glog.V(3).Infof("Command: %q TestResult: <<EMPTY>> \n", lastCommand)
|
|
}
|
|
|
|
if c.Reason != "" {
|
|
glog.V(2).Info(c.Reason)
|
|
}
|
|
return c.State
|
|
}
|
|
|
|
func (c *Check) runAuditCommands() (lastCommand string, err error) {
|
|
// Always run auditEnvOutput if needed
|
|
if c.AuditEnv != "" {
|
|
c.AuditEnvOutput, err = runAudit(c.AuditEnv)
|
|
if err != nil {
|
|
return c.AuditEnv, err
|
|
}
|
|
}
|
|
|
|
// Run the audit command and auditConfig commands, if present
|
|
c.AuditOutput, err = runAudit(c.Audit)
|
|
if err != nil {
|
|
return c.Audit, err
|
|
}
|
|
|
|
c.AuditConfigOutput, err = runAudit(c.AuditConfig)
|
|
// when file not found then error comes as exit status 127
|
|
// in some env same error comes as exit status 1
|
|
if err != nil && (strings.Contains(err.Error(), "exit status 127") ||
|
|
strings.Contains(err.Error(), "No such file or directory")) &&
|
|
(c.AuditEnvOutput != "" || c.AuditOutput != "") {
|
|
// suppress file not found error when there is Audit OR auditEnv output present
|
|
glog.V(3).Info(err)
|
|
err = nil
|
|
c.AuditConfigOutput = ""
|
|
}
|
|
return c.AuditConfig, err
|
|
}
|
|
|
|
func (c *Check) execute() (finalOutput *testOutput, err error) {
|
|
finalOutput = &testOutput{}
|
|
|
|
ts := c.Tests
|
|
res := make([]testOutput, len(ts.TestItems))
|
|
expectedResultArr := make([]string, len(res))
|
|
|
|
glog.V(3).Infof("Running %d test_items", len(ts.TestItems))
|
|
for i, t := range ts.TestItems {
|
|
|
|
t.isMultipleOutput = c.IsMultiple
|
|
|
|
// Try with the auditOutput first, and if that's not found, try the auditConfigOutput
|
|
t.auditUsed = AuditCommand
|
|
result := *(t.execute(c.AuditOutput))
|
|
|
|
// Check for AuditConfigOutput only if AuditConfig is set and auditConfigOutput is not empty
|
|
if !result.flagFound && c.AuditConfig != "" && c.AuditConfigOutput != "" {
|
|
// t.isConfigSetting = true
|
|
t.auditUsed = AuditConfig
|
|
result = *(t.execute(c.AuditConfigOutput))
|
|
if !result.flagFound && t.Env != "" {
|
|
t.auditUsed = AuditEnv
|
|
result = *(t.execute(c.AuditEnvOutput))
|
|
}
|
|
}
|
|
|
|
if !result.flagFound && t.Env != "" {
|
|
t.auditUsed = AuditEnv
|
|
result = *(t.execute(c.AuditEnvOutput))
|
|
}
|
|
glog.V(2).Infof("Used %s", t.auditUsed)
|
|
res[i] = result
|
|
expectedResultArr[i] = res[i].ExpectedResult
|
|
}
|
|
|
|
var result bool
|
|
// If no binary operation is specified, default to AND
|
|
switch ts.BinOp {
|
|
default:
|
|
glog.V(2).Info(fmt.Sprintf("unknown binary operator for tests %s\n", ts.BinOp))
|
|
finalOutput.actualResult = fmt.Sprintf("unknown binary operator for tests %s\n", ts.BinOp)
|
|
return finalOutput, fmt.Errorf("unknown binary operator for tests %s", ts.BinOp)
|
|
case and, "":
|
|
result = true
|
|
for i := range res {
|
|
result = result && res[i].testResult
|
|
}
|
|
// Generate an AND expected result
|
|
finalOutput.ExpectedResult = strings.Join(expectedResultArr, " AND ")
|
|
|
|
case or:
|
|
result = false
|
|
for i := range res {
|
|
result = result || res[i].testResult
|
|
}
|
|
// Generate an OR expected result
|
|
finalOutput.ExpectedResult = strings.Join(expectedResultArr, " OR ")
|
|
}
|
|
|
|
finalOutput.testResult = result
|
|
finalOutput.actualResult = res[0].actualResult
|
|
|
|
glog.V(3).Infof("Returning from execute on tests: finalOutput %#v", finalOutput)
|
|
return finalOutput, nil
|
|
}
|
|
|
|
func runAudit(audit string) (output string, err error) {
|
|
var out bytes.Buffer
|
|
|
|
audit = strings.TrimSpace(audit)
|
|
if len(audit) == 0 {
|
|
return output, err
|
|
}
|
|
|
|
cmd := exec.Command("/bin/sh")
|
|
cmd.Stdin = strings.NewReader(audit)
|
|
cmd.Stdout = &out
|
|
cmd.Stderr = &out
|
|
err = cmd.Run()
|
|
output = out.String()
|
|
|
|
if err != nil {
|
|
err = fmt.Errorf("failed to run: %q, output: %q, error: %s", audit, output, err)
|
|
} else {
|
|
glog.V(3).Infof("Command: %q", audit)
|
|
glog.V(3).Infof("Output:\n %q", output)
|
|
}
|
|
return output, err
|
|
}
|