mirror of
https://github.com/aquasecurity/kube-bench.git
synced 2024-11-22 16:18:07 +00:00
Run audit as shell script instead of as single line command (#610)
* Run audit as shell script instead of as single line command * Rename runExecCommands to runAudit * Fix tests Co-authored-by: Liz Rice <liz@lizrice.com>
This commit is contained in:
parent
122bc4b351
commit
c7b518e76b
152
check/check.go
152
check/check.go
@ -17,10 +17,7 @@ package check
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
@ -70,8 +67,6 @@ type Check struct {
|
|||||||
Audit string `json:"audit"`
|
Audit string `json:"audit"`
|
||||||
AuditConfig string `yaml:"audit_config"`
|
AuditConfig string `yaml:"audit_config"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Commands []*exec.Cmd `json:"omit"`
|
|
||||||
ConfigCommands []*exec.Cmd `json:"omit"`
|
|
||||||
Tests *tests `json:"omit"`
|
Tests *tests `json:"omit"`
|
||||||
Set bool `json:"omit"`
|
Set bool `json:"omit"`
|
||||||
Remediation string `json:"remediation"`
|
Remediation string `json:"remediation"`
|
||||||
@ -128,9 +123,9 @@ func (c *Check) run() State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lastCommand := c.Audit
|
lastCommand := c.Audit
|
||||||
hasAuditConfig := c.ConfigCommands != nil
|
hasAuditConfig := c.AuditConfig != ""
|
||||||
|
|
||||||
state, finalOutput, retErrmsgs := performTest(c.Audit, c.Commands, c.Tests)
|
state, finalOutput, retErrmsgs := performTest(c.Audit, c.Tests)
|
||||||
if len(state) > 0 {
|
if len(state) > 0 {
|
||||||
c.Reason = retErrmsgs
|
c.Reason = retErrmsgs
|
||||||
c.State = state
|
c.State = state
|
||||||
@ -166,7 +161,7 @@ func (c *Check) run() State {
|
|||||||
currentTests.TestItems[i] = nti
|
currentTests.TestItems[i] = nti
|
||||||
}
|
}
|
||||||
|
|
||||||
state, finalOutput, retErrmsgs = performTest(c.AuditConfig, c.ConfigCommands, currentTests)
|
state, finalOutput, retErrmsgs = performTest(c.AuditConfig, currentTests)
|
||||||
if len(state) > 0 {
|
if len(state) > 0 {
|
||||||
c.Reason = retErrmsgs
|
c.Reason = retErrmsgs
|
||||||
c.State = state
|
c.State = state
|
||||||
@ -200,78 +195,13 @@ func (c *Check) run() State {
|
|||||||
return c.State
|
return c.State
|
||||||
}
|
}
|
||||||
|
|
||||||
// textToCommand transforms an input text representation of commands to be
|
func performTest(audit string, tests *tests) (State, *testOutput, string) {
|
||||||
// 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 {
|
if len(strings.TrimSpace(audit)) == 0 {
|
||||||
return "", failTestItem("missing command"), "missing audit command"
|
return "", failTestItem("missing command"), "missing audit command"
|
||||||
}
|
}
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
state, retErrmsgs := runExecCommands(audit, commands, &out)
|
errmsgs := runAudit(audit, &out)
|
||||||
if len(state) > 0 {
|
|
||||||
return state, nil, retErrmsgs
|
|
||||||
}
|
|
||||||
errmsgs := retErrmsgs
|
|
||||||
|
|
||||||
finalOutput := tests.execute(out.String())
|
finalOutput := tests.execute(out.String())
|
||||||
if finalOutput == nil {
|
if finalOutput == nil {
|
||||||
@ -281,73 +211,17 @@ func performTest(audit string, commands []*exec.Cmd, tests *tests) (State, *test
|
|||||||
return "", finalOutput, errmsgs
|
return "", finalOutput, errmsgs
|
||||||
}
|
}
|
||||||
|
|
||||||
func runExecCommands(audit string, commands []*exec.Cmd, out *bytes.Buffer) (State, string) {
|
func runAudit(audit string, out *bytes.Buffer) string {
|
||||||
var err error
|
|
||||||
errmsgs := ""
|
errmsgs := ""
|
||||||
|
|
||||||
// Check if command exists or exit with WARN.
|
cmd := exec.Command("/bin/sh")
|
||||||
for _, cmd := range commands {
|
cmd.Stdin = strings.NewReader(audit)
|
||||||
if !isShellCommand(cmd.Path) {
|
cmd.Stdout = out
|
||||||
errmsgs += fmt.Sprintf("Command '%s' not found\n", cmd.Path)
|
cmd.Stderr = out
|
||||||
return WARN, errmsgs
|
if err := cmd.Run(); err != nil {
|
||||||
}
|
errmsgs += fmt.Sprintf("failed to run: %q, output: %q, error: %s\n", audit, out.String(), err)
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
glog.V(3).Infof("Command %q - Output:\n\n %q\n - Error Messages:%q \n", audit, out.String(), errmsgs)
|
||||||
return "", 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)
|
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,8 @@
|
|||||||
package check
|
package check
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os/exec"
|
"bytes"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -33,7 +34,7 @@ func TestCheck_Run(t *testing.T) {
|
|||||||
{
|
{
|
||||||
check: Check{ // Not scored checks with passing tests are marked pass
|
check: Check{ // Not scored checks with passing tests are marked pass
|
||||||
Scored: false,
|
Scored: false,
|
||||||
Audit: ":", Commands: []*exec.Cmd{exec.Command("")},
|
Audit: ":",
|
||||||
Tests: &tests{TestItems: []*testItem{&testItem{}}},
|
Tests: &tests{TestItems: []*testItem{&testItem{}}},
|
||||||
},
|
},
|
||||||
Expected: PASS,
|
Expected: PASS,
|
||||||
@ -44,7 +45,7 @@ func TestCheck_Run(t *testing.T) {
|
|||||||
{
|
{
|
||||||
check: Check{ // Scored checks with passing tests are marked pass
|
check: Check{ // Scored checks with passing tests are marked pass
|
||||||
Scored: true,
|
Scored: true,
|
||||||
Audit: ":", Commands: []*exec.Cmd{exec.Command("")},
|
Audit: ":",
|
||||||
Tests: &tests{TestItems: []*testItem{&testItem{}}},
|
Tests: &tests{TestItems: []*testItem{&testItem{}}},
|
||||||
},
|
},
|
||||||
Expected: PASS,
|
Expected: PASS,
|
||||||
@ -111,3 +112,66 @@ func TestCheckAuditConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_runAudit(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
audit string
|
||||||
|
out *bytes.Buffer
|
||||||
|
output string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
errMsg string
|
||||||
|
output string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "run success",
|
||||||
|
args: args{
|
||||||
|
audit: "echo 'hello world'",
|
||||||
|
out: &bytes.Buffer{},
|
||||||
|
},
|
||||||
|
errMsg: "",
|
||||||
|
output: "hello world\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "run multiple lines script",
|
||||||
|
args: args{
|
||||||
|
audit: `
|
||||||
|
hello() {
|
||||||
|
echo "hello world"
|
||||||
|
}
|
||||||
|
|
||||||
|
hello
|
||||||
|
`,
|
||||||
|
out: &bytes.Buffer{},
|
||||||
|
},
|
||||||
|
errMsg: "",
|
||||||
|
output: "hello world\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "run failed",
|
||||||
|
args: args{
|
||||||
|
audit: "unknown_command",
|
||||||
|
out: &bytes.Buffer{},
|
||||||
|
},
|
||||||
|
errMsg: "failed to run: \"unknown_command\", output: \"/bin/sh: ",
|
||||||
|
output: "not found\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
errMsg := runAudit(tt.args.audit, tt.args.out)
|
||||||
|
if errMsg != "" && !strings.Contains(errMsg, tt.errMsg) {
|
||||||
|
t.Errorf("runAudit() errMsg = %q, want %q", errMsg, tt.errMsg)
|
||||||
|
}
|
||||||
|
output := tt.args.out.String()
|
||||||
|
if errMsg == "" && output != tt.output {
|
||||||
|
t.Errorf("runAudit() output = %q, want %q", output, tt.output)
|
||||||
|
}
|
||||||
|
if errMsg != "" && !strings.Contains(output, tt.output) {
|
||||||
|
t.Errorf("runAudit() output = %q, want %q", output, tt.output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -70,18 +70,6 @@ func NewControls(t NodeType, in []byte) (*Controls, error) {
|
|||||||
return nil, fmt.Errorf("non-%s controls file specified", t)
|
return nil, fmt.Errorf("non-%s controls file specified", t)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare audit commands
|
|
||||||
for _, group := range c.Groups {
|
|
||||||
for _, check := range group.Checks {
|
|
||||||
glog.V(3).Infof("Check.ID %s", check.ID)
|
|
||||||
check.Commands = textToCommand(check.Audit)
|
|
||||||
if len(check.AuditConfig) > 0 {
|
|
||||||
glog.V(3).Infof("Check.ID has audit_config %s", check.ID)
|
|
||||||
check.ConfigCommands = textToCommand(check.AuditConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user