Skip to content

Commit

Permalink
chore: make integration tests more robust
Browse files Browse the repository at this point in the history
- add generic functions to check received events in different manners:
  - ExpectAllInOrder
  - ExpectAllEqualTo
  - ExpectAtLeastOneOfEach
- add exec.go with helpers to run cmds via fork/exec and fork/syscall
- add waitForTraceeOutputEvents function to wait for tracee output
  events until buffer fills with certain number of events or
  timeout occurs
- remove tester.sh concentrating all event filter tests in
  event_filters_test.go
  • Loading branch information
geyslan committed May 8, 2023
1 parent 52da4f0 commit e415a35
Show file tree
Hide file tree
Showing 8 changed files with 1,631 additions and 641 deletions.
1,158 changes: 1,158 additions & 0 deletions tests/integration/event_filters_test.go

Large diffs are not rendered by default.

216 changes: 216 additions & 0 deletions tests/integration/exec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
package integration

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"syscall"
"time"

"golang.org/x/sys/unix"

"github.com/aquasecurity/tracee/pkg/events"
"github.com/aquasecurity/tracee/types/trace"
)

var maxCPU = 1 // maximum number of CPUs to pin processes to

const cpuForTests = 0 // CPU to pin test processes to

// setCPUs pins the current process to a specific CPU
func setCPUs(id ...int) {
cpuMask := unix.CPUSet{}
for _, i := range id {
cpuMask.Set(i)
}
_ = unix.SchedSetaffinity(0, &cpuMask)
}

const pattern = `'[^']*'|\S+` // split on spaces, but not spaces inside single quotes

var re = regexp.MustCompile(pattern)

// parseCmd parses a command string into a command and arguments
func parseCmd(fullCmd string) (string, []string, error) {
vals := re.FindAllString(fullCmd, -1)

if len(vals) == 0 {
return "", nil, fmt.Errorf("no command specified")
}
cmd := vals[0]
cmd, err := exec.LookPath(cmd)
if err != nil {
return "", nil, err
}
if !filepath.IsAbs(cmd) {
cmd, err = filepath.Abs(cmd)
if err != nil {
return "", nil, err
}
}

args := vals
// remove single quotes from args, since they can confuse exec
for i, arg := range args {
args[i] = strings.Trim(arg, "'")
}

return cmd, args, nil
}

// handleFDs closes all fds except for stdin, stdout, stderr
// It also redirects stdout to /dev/null
func handleFDs() error {
// close range of fds
err := unix.CloseRange(3, ^uint(0), unix.CLOSE_RANGE_UNSHARE)
if err != nil {
return fmt.Errorf("close range of fds: %w", err)
}

// redirect stdout to /dev/null
err = syscall.Close(syscall.Stdout)
if err != nil {
return fmt.Errorf("close: %w", err)
}
_, err = syscall.Open(os.DevNull, os.O_WRONLY, syscall.S_IRWXU)
if err != nil {
return fmt.Errorf("open %s: %w", os.DevNull, err)
}

return nil
}

// waitChild waits for a child process to exit
func waitChild(pid int, timeout time.Duration) (int, error) {
var (
ticker = time.NewTicker(100 * time.Millisecond)
wopts = syscall.WNOHANG | syscall.WUNTRACED | syscall.WCONTINUED
wstat syscall.WaitStatus
)

for {
select {
case <-ticker.C:
ret, err := syscall.Wait4(pid, &wstat, wopts, nil)
if err != nil {
return -1, err
}

if ret != 0 { // child process has exited
if wstat.Exited() {
return pid, nil
}

return -1, fmt.Errorf("child process exited with status: %v", wstat)
}

// check if timeout has occurred
case <-time.After(timeout):
_ = syscall.Kill(-pid, syscall.SIGTERM) // kill child and all of its children
return -1, fmt.Errorf("timed out waiting for child process to exit")
}
}
}

// forkAndExec forks and execs a command
// It returns the pid of the child process
func forkAndExec(cmd string, timeout time.Duration) (int, error) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()

pid, _, errno := syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0)
if pid == 0 { // child
setCPUs(cpuForTests)
err := handleFDs()
if err != nil {
fmt.Fprintf(os.Stderr, "handleFDs: %v", err)
syscall.Exit(1)
}

// set process group id to be the same as pid
err = syscall.Setpgid(0, 0)
if err != nil {
fmt.Fprintf(os.Stderr, "Setpgid: %v", err)
syscall.Exit(1)
}

cmd, args, err := parseCmd(cmd)
if err != nil {
fmt.Fprintf(os.Stderr, "parseCmd: %v", err)
syscall.Exit(1)
}
fmt.Fprintf(os.Stderr, "\texecuting: %s %v\n", cmd, args)

// exec
err = syscall.Exec(cmd, args, os.Environ())
// point of no return, but we need to handle err in case exec fails
if err != nil {
fmt.Fprintf(os.Stderr, "Exec: %v", err)
syscall.Exit(1)
}
}

if int(pid) == -1 {
return -1, syscall.Errno(errno)
}

return waitChild(int(pid), timeout)
}

// forkAndCallSys forks and calls the syscalls specified in evts, changing the child's comm to newComm
// It returns the pid of the child process
func forkAndCallSys(newComm string, evts []trace.Event, timeout time.Duration) (int, error) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()

pid, _, errno := syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0)
if pid == 0 { // child
setCPUs(cpuForTests)
err := handleFDs()
if err != nil {
fmt.Fprintf(os.Stderr, "handleFDs: %v", err)
syscall.Exit(1)
}

// set process group id to be the same as pid
err = syscall.Setpgid(0, 0)
if err != nil {
fmt.Fprintf(os.Stderr, "Setpgid: %v", err)
syscall.Exit(1)
}

evtsID := make([]events.ID, 0)
eventsDefinitions := events.Definitions
fmt.Fprintf(os.Stderr, "\tcalling events:\n")
for _, evt := range evts {
evtsID = append(evtsID, events.ID(evt.EventID))
e := eventsDefinitions.Get(events.ID(evt.EventID))
fmt.Fprintf(os.Stderr, "\t\t%v %s\n", evt.EventID, e.Name)
}

//
// print everything we need above, before changing comm, so we don't taint the events output
//

err = changeOwnComm(newComm)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
syscall.Exit(1)
}

// we can't make any other syscalls after this point but the ones we want to trace
_ = callsys(evtsID)

syscall.Exit(0)
}

if int(pid) == -1 {
return -1, syscall.Errno(errno)
}

return waitChild(int(pid), timeout)
}
66 changes: 66 additions & 0 deletions tests/integration/exec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package integration

import (
"testing"

"github.com/stretchr/testify/assert"
)

// Test_ParseCmd tests the parseCmd function
func Test_ParseCmd(t *testing.T) {
tt := []struct {
input string
expectedCmd string
expectedArgs []string
expectedErrMsg string
}{
{
input: "",
expectedCmd: "",
expectedArgs: nil,
expectedErrMsg: "no command specified",
},
{
input: "echo hello",
expectedCmd: "/usr/bin/echo",
expectedArgs: []string{"echo", "hello"},
expectedErrMsg: "",
},
{
input: "/usr/bin/echo hello",
expectedCmd: "/usr/bin/echo",
expectedArgs: []string{"/usr/bin/echo", "hello"},
expectedErrMsg: "",
},
{
input: "echo 'hello world'",
expectedCmd: "/usr/bin/echo",
expectedArgs: []string{"echo", "hello world"},
expectedErrMsg: "",
},
{
input: "bash -c 'echo hello world'",
expectedCmd: "/usr/bin/bash",
expectedArgs: []string{"bash", "-c", "echo hello world"},
expectedErrMsg: "",
},
{
input: "invalidcommand",
expectedCmd: "",
expectedArgs: nil,
expectedErrMsg: "exec: \"invalidcommand\": executable file not found in $PATH",
},
}

for _, tc := range tt {
cmd, args, err := parseCmd(tc.input)

if err == nil {
assert.Equal(t, tc.expectedCmd, cmd)
assert.Len(t, args, len(tc.expectedArgs))
assert.Equal(t, tc.expectedArgs, args)
} else if err.Error() != tc.expectedErrMsg {
t.Errorf("For input \"%s\", expected error message \"%s\", but got \"%s\"", tc.input, tc.expectedErrMsg, err)
}
}
}
Loading

0 comments on commit e415a35

Please sign in to comment.