From c7b518e76bca0855b443215816b2874f8eceff2f Mon Sep 17 00:00:00 2001 From: Huang Huang Date: Mon, 22 Jun 2020 15:45:31 +0800 Subject: [PATCH] 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 --- check/check.go | 172 ++++++-------------------------------------- check/check_test.go | 74 +++++++++++++++++-- check/controls.go | 12 ---- 3 files changed, 92 insertions(+), 166 deletions(-) diff --git a/check/check.go b/check/check.go index bd8bcb9..ab6edb8 100644 --- a/check/check.go +++ b/check/check.go @@ -17,10 +17,7 @@ package check import ( "bytes" "fmt" - "io" - "os" "os/exec" - "regexp" "strings" "github.com/golang/glog" @@ -65,22 +62,20 @@ const ( // 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"` + 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"` + 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"` + Reason string `json:"reason,omitempty"` } // Runner wraps the basic Run method. @@ -128,9 +123,9 @@ func (c *Check) run() State { } 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 { c.Reason = retErrmsgs c.State = state @@ -166,7 +161,7 @@ func (c *Check) run() State { currentTests.TestItems[i] = nti } - state, finalOutput, retErrmsgs = performTest(c.AuditConfig, c.ConfigCommands, currentTests) + state, finalOutput, retErrmsgs = performTest(c.AuditConfig, currentTests) if len(state) > 0 { c.Reason = retErrmsgs c.State = state @@ -200,78 +195,13 @@ func (c *Check) run() State { 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) { +func performTest(audit string, 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 + errmsgs := runAudit(audit, &out) finalOutput := tests.execute(out.String()) if finalOutput == nil { @@ -281,73 +211,17 @@ func performTest(audit string, commands []*exec.Cmd, tests *tests) (State, *test return "", finalOutput, errmsgs } -func runExecCommands(audit string, commands []*exec.Cmd, out *bytes.Buffer) (State, string) { - var err error +func runAudit(audit string, out *bytes.Buffer) string { 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++ + cmd := exec.Command("/bin/sh") + cmd.Stdin = strings.NewReader(audit) + cmd.Stdout = out + cmd.Stderr = out + if err := cmd.Run(); err != nil { + errmsgs += fmt.Sprintf("failed to run: %q, output: %q, error: %s\n", audit, out.String(), err) } 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) + return errmsgs } diff --git a/check/check_test.go b/check/check_test.go index 4d4e695..45c37ec 100644 --- a/check/check_test.go +++ b/check/check_test.go @@ -15,7 +15,8 @@ package check import ( - "os/exec" + "bytes" + "strings" "testing" ) @@ -33,8 +34,8 @@ func TestCheck_Run(t *testing.T) { { check: Check{ // Not scored checks with passing tests are marked pass Scored: false, - Audit: ":", Commands: []*exec.Cmd{exec.Command("")}, - Tests: &tests{TestItems: []*testItem{&testItem{}}}, + Audit: ":", + Tests: &tests{TestItems: []*testItem{&testItem{}}}, }, Expected: PASS, }, @@ -44,8 +45,8 @@ func TestCheck_Run(t *testing.T) { { check: Check{ // Scored checks with passing tests are marked pass Scored: true, - Audit: ":", Commands: []*exec.Cmd{exec.Command("")}, - Tests: &tests{TestItems: []*testItem{&testItem{}}}, + Audit: ":", + Tests: &tests{TestItems: []*testItem{&testItem{}}}, }, 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) + } + }) + } +} diff --git a/check/controls.go b/check/controls.go index 21d95d9..1b36dee 100644 --- a/check/controls.go +++ b/check/controls.go @@ -70,18 +70,6 @@ func NewControls(t NodeType, in []byte) (*Controls, error) { 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 }