-
Notifications
You must be signed in to change notification settings - Fork 394
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: make integration tests more robust
- 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
Showing
8 changed files
with
1,631 additions
and
641 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |
Oops, something went wrong.