Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run audit as shell script instead of as single line command #610

Merged
merged 5 commits into from Jun 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
172 changes: 23 additions & 149 deletions check/check.go
Expand Up @@ -17,10 +17,7 @@ package check
import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"regexp"
"strings"

"github.com/golang/glog"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
74 changes: 69 additions & 5 deletions check/check_test.go
Expand Up @@ -15,7 +15,8 @@
package check

import (
"os/exec"
"bytes"
"strings"
"testing"
)

Expand All @@ -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,
},
Expand All @@ -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,
},
Expand Down Expand Up @@ -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)
}
})
}
}
12 changes: 0 additions & 12 deletions check/controls.go
Expand Up @@ -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
}

Expand Down