mirror of
https://github.com/aquasecurity/kube-bench.git
synced 2024-12-24 07:28:06 +00:00
06303f6a7a
* Update check.go Added new warn_reason value which gives a brief explanation about why the not scored tests failed * Update common.go Changed when a not scored test fails because it has a wrong syntax audit command or just running something that can't be run the print the failure. but if the test just fails because it doesn't line up with the cis hardening recommendations then print the remediation text. * Update check/check.go fix typo Co-Authored-By: Liz Rice <liz@lizrice.com> * Update check.go * Update common.go * Update check.go added back os.Exit(1) to exitWithError * Update job-master.data Change some tests output to fit warn reason. (No change to the summary) * Update job-node.data Changed some tests output to fit warn reason. (No change to the summary) * Update job.data Change some tests output to fit warn reason. (No change to the summary) * Update common.go Keep to old way to print manual test output Co-authored-by: Liz Rice <liz@lizrice.com> Co-authored-by: Roberto Rojas <robertojrojas@gmail.com>
354 lines
8.7 KiB
Go
354 lines
8.7 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"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"regexp"
|
|
"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"
|
|
|
|
// 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"`
|
|
AuditConfig string `yaml:"audit_config"`
|
|
Type string `json:"type"`
|
|
Commands []*exec.Cmd `json:"omit"`
|
|
ConfigCommands []*exec.Cmd `json:"omit"`
|
|
Tests *tests `json:"omit"`
|
|
Set bool `json:"omit"`
|
|
Remediation string `json:"remediation"`
|
|
TestInfo []string `json:"test_info"`
|
|
State `json:"status"`
|
|
ActualValue string `json:"actual_value"`
|
|
Scored bool `json:"scored"`
|
|
ExpectedResult string `json:"expected_result"`
|
|
Reason string `json:"reason,omitempty"`
|
|
}
|
|
|
|
// 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 {
|
|
|
|
// Since this is an Scored check
|
|
// without tests return a 'WARN' to alert
|
|
// the user that this check needs attention
|
|
if c.Scored && len(strings.TrimSpace(c.Type)) == 0 && c.Tests == nil {
|
|
c.Reason = "There are no tests"
|
|
c.State = WARN
|
|
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
|
|
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
|
|
return c.State
|
|
}
|
|
|
|
lastCommand := c.Audit
|
|
hasAuditConfig := c.ConfigCommands != nil
|
|
|
|
state, finalOutput, retErrmsgs := performTest(c.Audit, c.Commands, c.Tests)
|
|
if len(state) > 0 {
|
|
c.Reason = retErrmsgs
|
|
c.State = state
|
|
return c.State
|
|
}
|
|
errmsgs := retErrmsgs
|
|
|
|
// If something went wrong with the 'Audit' command
|
|
// and an 'AuditConfig' command was provided, use it to
|
|
// execute tests
|
|
if (finalOutput == nil || !finalOutput.testResult) && hasAuditConfig {
|
|
lastCommand = c.AuditConfig
|
|
|
|
nItems := len(c.Tests.TestItems)
|
|
// The reason we're creating a copy of the "tests"
|
|
// is so that tests can executed
|
|
// with the AuditConfig command
|
|
// against the Path only
|
|
currentTests := &tests{
|
|
BinOp: c.Tests.BinOp,
|
|
TestItems: make([]*testItem, nItems),
|
|
}
|
|
|
|
for i := 0; i < nItems; i++ {
|
|
ti := c.Tests.TestItems[i]
|
|
nti := &testItem{
|
|
// Path is used to test Command Param values
|
|
// AuditConfig ==> Path
|
|
Path: ti.Path,
|
|
Set: ti.Set,
|
|
Compare: ti.Compare,
|
|
}
|
|
currentTests.TestItems[i] = nti
|
|
}
|
|
|
|
state, finalOutput, retErrmsgs = performTest(c.AuditConfig, c.ConfigCommands, currentTests)
|
|
if len(state) > 0 {
|
|
c.Reason = retErrmsgs
|
|
c.State = state
|
|
return c.State
|
|
}
|
|
errmsgs += retErrmsgs
|
|
}
|
|
|
|
if finalOutput != nil && finalOutput.testResult {
|
|
c.State = PASS
|
|
c.ActualValue = finalOutput.actualResult
|
|
c.ExpectedResult = finalOutput.ExpectedResult
|
|
} else {
|
|
if c.Scored {
|
|
c.State = FAIL
|
|
} else {
|
|
c.Reason = errmsgs
|
|
c.State = WARN
|
|
}
|
|
}
|
|
|
|
if finalOutput != nil {
|
|
glog.V(3).Infof("Check.ID: %s Command: %q TestResult: %t State: %q \n", c.ID, lastCommand, finalOutput.testResult, c.State)
|
|
} else {
|
|
glog.V(3).Infof("Check.ID: %s Command: %q TestResult: <<EMPTY>> \n", c.ID, lastCommand)
|
|
}
|
|
|
|
if errmsgs != "" {
|
|
glog.V(2).Info(errmsgs)
|
|
}
|
|
return c.State
|
|
}
|
|
|
|
// textToCommand transforms an input text representation of commands to be
|
|
// run into a slice of commands.
|
|
// TODO: Make this more robust.
|
|
func textToCommand(s string) []*exec.Cmd {
|
|
glog.V(3).Infof("textToCommand: %q\n", s)
|
|
cmds := []*exec.Cmd{}
|
|
|
|
cp := strings.Split(s, "|")
|
|
|
|
for _, v := range cp {
|
|
v = strings.Trim(v, " ")
|
|
|
|
// TODO:
|
|
// GOAL: To split input text into arguments for exec.Cmd.
|
|
//
|
|
// CHALLENGE: The input text may contain quoted strings that
|
|
// must be passed as a unit to exec.Cmd.
|
|
// eg. bash -c 'foo bar'
|
|
// 'foo bar' must be passed as unit to exec.Cmd if not the command
|
|
// will fail when it is executed.
|
|
// eg. exec.Cmd("bash", "-c", "foo bar")
|
|
//
|
|
// PROBLEM: Current solution assumes the grouped string will always
|
|
// be at the end of the input text.
|
|
re := regexp.MustCompile(`^(.*)(['"].*['"])$`)
|
|
grps := re.FindStringSubmatch(v)
|
|
|
|
var cs []string
|
|
if len(grps) > 0 {
|
|
s := strings.Trim(grps[1], " ")
|
|
cs = strings.Split(s, " ")
|
|
|
|
s1 := grps[len(grps)-1]
|
|
s1 = strings.Trim(s1, "'\"")
|
|
|
|
cs = append(cs, s1)
|
|
} else {
|
|
cs = strings.Split(v, " ")
|
|
}
|
|
|
|
cmd := exec.Command(cs[0], cs[1:]...)
|
|
cmds = append(cmds, cmd)
|
|
}
|
|
|
|
return cmds
|
|
}
|
|
|
|
func isShellCommand(s string) bool {
|
|
cmd := exec.Command("/bin/sh", "-c", "command -v "+s)
|
|
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
exitWithError(fmt.Errorf("failed to check if command: %q is valid %v", s, err))
|
|
}
|
|
|
|
if strings.Contains(string(out), s) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func performTest(audit string, commands []*exec.Cmd, tests *tests) (State, *testOutput, string) {
|
|
if len(strings.TrimSpace(audit)) == 0 {
|
|
return "", failTestItem("missing command"), "missing audit command"
|
|
}
|
|
|
|
var out bytes.Buffer
|
|
state, retErrmsgs := runExecCommands(audit, commands, &out)
|
|
if len(state) > 0 {
|
|
return state, nil, retErrmsgs
|
|
}
|
|
errmsgs := retErrmsgs
|
|
|
|
finalOutput := tests.execute(out.String())
|
|
if finalOutput == nil {
|
|
errmsgs += fmt.Sprintf("Final output is <<EMPTY>>. Failed to run: %s\n", audit)
|
|
}
|
|
|
|
return "", finalOutput, errmsgs
|
|
}
|
|
|
|
func runExecCommands(audit string, commands []*exec.Cmd, out *bytes.Buffer) (State, string) {
|
|
var err error
|
|
errmsgs := ""
|
|
|
|
// Check if command exists or exit with WARN.
|
|
for _, cmd := range commands {
|
|
if !isShellCommand(cmd.Path) {
|
|
errmsgs += fmt.Sprintf("Command '%s' not found\n", cmd.Path)
|
|
return WARN, errmsgs
|
|
}
|
|
}
|
|
|
|
// Run commands.
|
|
n := len(commands)
|
|
if n == 0 {
|
|
// Likely a warning message.
|
|
return WARN, errmsgs
|
|
}
|
|
|
|
// Each command runs,
|
|
// cmd0 out -> cmd1 in, cmd1 out -> cmd2 in ... cmdn out -> os.stdout
|
|
// cmd0 err should terminate chain
|
|
cs := commands
|
|
|
|
// Initialize command pipeline
|
|
cs[n-1].Stdout = out
|
|
i := 1
|
|
|
|
for i < n {
|
|
cs[i-1].Stdout, err = cs[i].StdinPipe()
|
|
if err != nil {
|
|
errmsgs += fmt.Sprintf("failed to run: %s, command: %s, error: %s\n", audit, cs[i].Args, err)
|
|
}
|
|
i++
|
|
}
|
|
|
|
// Start command pipeline
|
|
i = 0
|
|
for i < n {
|
|
err := cs[i].Start()
|
|
if err != nil {
|
|
errmsgs += fmt.Sprintf("failed to run: %s, command: %s, error: %s\n", audit, cs[i].Args, err)
|
|
}
|
|
i++
|
|
}
|
|
|
|
// Complete command pipeline
|
|
i = 0
|
|
for i < n {
|
|
err := cs[i].Wait()
|
|
if err != nil {
|
|
errmsgs += fmt.Sprintf("failed to run: %s, command: %s, error: %s\n", audit, cs[i].Args, err)
|
|
}
|
|
|
|
if i < n-1 {
|
|
cs[i].Stdout.(io.Closer).Close()
|
|
}
|
|
i++
|
|
}
|
|
|
|
glog.V(3).Infof("Command %q - Output:\n\n %q\n - Error Messages:%q \n", audit, out.String(), errmsgs)
|
|
return "", errmsgs
|
|
}
|
|
|
|
func exitWithError(err error) {
|
|
fmt.Fprintf(os.Stderr, "\n%v\n", err)
|
|
// flush before exit non-zero
|
|
glog.Flush()
|
|
os.Exit(1)
|
|
}
|