diff --git a/.gitignore b/.gitignore index eccd8913857e..27af6d0b8a82 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,4 @@ compile_commands.json .vagrant # binaries and build files -dist \ No newline at end of file +dist diff --git a/tests/integration/event_filters_test.go b/tests/integration/event_filters_test.go new file mode 100644 index 000000000000..23ce03570d27 --- /dev/null +++ b/tests/integration/event_filters_test.go @@ -0,0 +1,1054 @@ +package integration + +import ( + "context" + "runtime" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/tracee/pkg/cmd/flags" + tracee "github.com/aquasecurity/tracee/pkg/ebpf" + "github.com/aquasecurity/tracee/pkg/events" + "github.com/aquasecurity/tracee/pkg/utils" + "github.com/aquasecurity/tracee/signatures/helpers" + "github.com/aquasecurity/tracee/types/trace" +) + +// Test_EventFilters tests a variety of trace event filters +// with different combinations of scopes +func Test_EventFilters(t *testing.T) { + // test table + tt := []struct { + name string + filterArgs []string + cmdEvents []cmdEvents + useSyscaller bool + test func(t *testing.T, cmdEvents []cmdEvents, actual *eventBuffer, useSyscaller bool) + }{ + // events matched in single scopes - detached workloads + { + name: "container: event: trace only events from new containers", + filterArgs: []string{"container=new", "event!=container_create,container_remove"}, // without prefix, scope is 1 + cmdEvents: []cmdEvents{ + newCmdEvents("docker run -d --rm alpine", + []trace.Event{ + expectEvent(anyHost, "sh", anyProcessorID, 1, 0, events.SchedProcessExec, orScopes(1)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllInOrder, + }, + { + name: "comm: event: trace events set in a single scope from ping command", + filterArgs: []string{ + "comm=ping", "event=sched_process_exec,sched_process_exit", + }, + cmdEvents: []cmdEvents{ + newCmdEvents("ping -c1 0.0.0.0", + []trace.Event{ + expectEvent(anyHost, "ping", cpuID, anyPID, 0, events.SchedProcessExec, orScopes(1)), + expectEvent(anyHost, "ping", cpuID, anyPID, 0, events.SchedProcessExit, orScopes(1)), + }, + []string{}, // no sets + ), + }, + useSyscaller: false, + test: ExpectAllInOrder, + }, + { + name: "comm: event: trace events set in a single scope from ping command", + filterArgs: []string{ + "5:comm=ping", "5:event=net_packet_icmp", + }, + cmdEvents: []cmdEvents{ + newCmdEvents("ping -c1 0.0.0.0", + []trace.Event{ + expectEvent(anyHost, "ping", cpuID, anyPID, 0, events.NetPacketICMP, orScopes(5)), + expectEvent(anyHost, "ping", cpuID, anyPID, 0, events.NetPacketICMP, orScopes(5)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllInOrder, + }, + { + name: "event: args: trace event set in a specific scope with args finishing with 'ls'", + filterArgs: []string{"59:event=execve", "59:execve.args.pathname=*ls"}, + cmdEvents: []cmdEvents{ + newCmdEvents("ls", + []trace.Event{ + expectEvent(anyHost, "integration.tes", // note that comm name is from the go test binary that runs the command + cpuID, anyPID, 0, events.Execve, orScopes(59), expectArg("pathname", "*ls")), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllInOrder, + }, + { + name: "event: args: trace event set in a specific scope with args starting with * wildcard", + filterArgs: []string{"59:event=execve", "59:execve.args.pathname=*/almost/improbable/path"}, // no event expected + cmdEvents: []cmdEvents{ + // no events expected + newCmdEvents("ls", []trace.Event{}, []string{}), + newCmdEvents("uname", []trace.Event{}, []string{}), + newCmdEvents("who", []trace.Event{}, []string{}), + }, + useSyscaller: false, + test: ExpectAllInOrder, + }, + { + name: "comm: event: args: trace event set in a specific scope with args from ls command", + filterArgs: []string{"42:comm=ls", "42:event=security_file_open", "42:security_file_open.args.pathname=*integration"}, + cmdEvents: []cmdEvents{ + newCmdEvents("ls", + []trace.Event{ + expectEvent(anyHost, "ls", cpuID, anyPID, 0, events.SecurityFileOpen, orScopes(42), expectArg("pathname", "*integration")), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllInOrder, + }, + { + name: "comm: event: trace events set in two specific scopes from ls and uname commands", + filterArgs: []string{ + "4:comm=ls", "4:event=sched_process_exit", + "2:comm=uname", "2:event=sched_process_exit", + }, + cmdEvents: []cmdEvents{ + newCmdEvents("ls", + []trace.Event{ + expectEvent(anyHost, "ls", cpuID, anyPID, 0, events.SchedProcessExit, orScopes(4)), + }, + []string{}, + ), + newCmdEvents("uname", + []trace.Event{ + expectEvent(anyHost, "uname", cpuID, anyPID, 0, events.SchedProcessExit, orScopes(2)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllInOrder, + }, + { + name: "bin: event: trace events in separate scopes from who and uname binary", + filterArgs: []string{ + "1:bin=/usr/bin/who", "1:event=sched_process_exec", + "2:bin=/usr/bin/uname", "2:event=sched_process_exec", + }, + cmdEvents: []cmdEvents{ + newCmdEvents("who", + []trace.Event{ + expectEvent(anyHost, "who", cpuID, anyPID, 0, events.SchedProcessExec, orScopes(1)), + }, + []string{}, + ), + newCmdEvents("uname", + []trace.Event{ + expectEvent(anyHost, "uname", cpuID, anyPID, 0, events.SchedProcessExec, orScopes(2)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllInOrder, + }, + // //TODO: Add pid>0 pid<1000 + // //TODO: Add u>0 u!=1000 + { + name: "pid: event: args: trace event sched_switch with args from pid 0", + filterArgs: []string{"pid=0", "event=sched_switch", "sched_switch.args.next_comm=systemd"}, + cmdEvents: []cmdEvents{ + newCmdEvents("kill -SIGHUP 1", // reloads the complete daemon configuration + []trace.Event{ + expectEvent(anyHost, anyComm, anyProcessorID, 0, 0, events.SchedSwitch, orScopes(1), expectArg("next_comm", "systemd")), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllEqualTo, + }, + { + name: "pid: trace events from pid 1", + filterArgs: []string{"pid=1"}, + cmdEvents: []cmdEvents{ + newCmdEvents("kill -SIGHUP 1", // reloads the complete daemon configuration + []trace.Event{ + expectEvent(anyHost, "systemd", anyProcessorID, 1, 0, anyEventID, orScopes(1)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllEqualTo, + }, + { + name: "uid: comm: trace uid 0 from ls command", + filterArgs: []string{"uid=0", "comm=ls"}, + cmdEvents: []cmdEvents{ + newCmdEvents("ls", + []trace.Event{ + expectEvent(anyHost, "ls", cpuID, anyPID, 0, anyEventID, orScopes(1)), + }, + []string{}, + ), + }, + test: ExpectAllEqualTo, + }, + { + name: "uid: comm: trace only uid>0 from ls command (should be empty)", + filterArgs: []string{"uid>0", "comm=ls"}, // no events expected + cmdEvents: []cmdEvents{ + newCmdEvents("ls", + []trace.Event{}, // no events expected + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllInOrder, + }, + { + name: "set: comm: trace filesystem events from ls command", + filterArgs: []string{"s=fs", "comm=ls"}, + cmdEvents: []cmdEvents{ + newCmdEvents("ls", + []trace.Event{ + expectEvent(anyHost, "ls", cpuID, anyPID, 0, anyEventID, orScopes(1)), + }, + []string{"fs"}, + ), + }, + useSyscaller: false, + test: ExpectAllEqualTo, + }, + { + name: "bin: event: trace only setns events from \"/usr/bin/dockerd\" binary", + filterArgs: []string{"bin=/usr/bin/dockerd", "event=setns"}, + cmdEvents: []cmdEvents{ + newCmdEvents("docker run -d --rm alpine", + []trace.Event{ + expectEvent(anyHost, "dockerd", anyProcessorID, anyPID, 0, events.Setns, orScopes(1)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllInOrder, + }, + + // TODO: To make this test really work, we need to check if a previous running pid is + // not detected. For that, we have to fork, to sleep and, only when tracee is + // ready, do syscalls from fork. + // { + // name: "trace new pids", + // filterArgs: []string{"pid=new"}, + // test: checkPidnew, + // }, + + // This uses the callsys helper which emits the desired events from a desired comm + // What is useful for testing events that are not easily triggered by a program + // In this simple example, we use it to: + // - impersonate a comm of "fakeprog1" + // - emit read and write events, as defined as expected events + { + name: "comm: event: trace events read and write set in a single scope from fakeprog1 command", + filterArgs: []string{ + "comm=fakeprog1", "event=read,write", + }, + cmdEvents: []cmdEvents{ + newCmdEvents("fakeprog1", + []trace.Event{ + expectEvent(anyHost, "fakeprog1", cpuID, anyPID, 0, events.Read, orScopes(1)), + expectEvent(anyHost, "fakeprog1", cpuID, anyPID, 0, events.Write, orScopes(1)), + }, + []string{}, + ), + }, + useSyscaller: true, + test: ExpectAllInOrder, + }, + { + name: "event: trace execve event set in a specific scope from fakeprog1 command", + filterArgs: []string{"59:event=execve"}, + cmdEvents: []cmdEvents{ + newCmdEvents("fakeprog1", + []trace.Event{ + expectEvent(anyHost, "fakeprog1", cpuID, anyPID, 0, events.Execve, orScopes(59)), + }, + []string{}, + ), + }, + useSyscaller: true, + test: ExpectAtLeastOneOfEach, + }, + { + name: "comm: event: args: trace event set in a specific scope with args from fakeprog1 and fakeprog2 commands", + filterArgs: []string{ + "64:comm=fakeprog1", "64:event=openat", "64:openat.args.dirfd=0", "64:openat.args.flags=0", "64:openat.args.mode=0", + "42:comm=fakeprog2", "42:event=open", "42:open.args.flags=0", "42:open.args.mode=0", + }, + cmdEvents: []cmdEvents{ + newCmdEvents("fakeprog1", + []trace.Event{ + expectEvent(anyHost, "fakeprog1", cpuID, anyPID, 0, events.Openat, orScopes(64), + expectArg("dirfd", int32(0)), + expectArg("flags", int32(0)), + expectArg("mode", uint32(0)), + ), + }, + []string{}, + ), + newCmdEvents("fakeprog2", + []trace.Event{ + expectEvent(anyHost, "fakeprog2", cpuID, anyPID, 0, events.Open, orScopes(42), + expectArg("flags", int32(0)), + expectArg("mode", uint32(0)), + ), + }, + []string{}, + ), + }, + useSyscaller: true, + test: ExpectAllInOrder, + }, + { + name: "comm: event: retval: trace event set in a specific scope with retval from fakeprog1 and fakeprog2 commands", + filterArgs: []string{ + "64:comm=fakeprog1", "64:event=openat", "64:openat.retval<0", + "42:comm=fakeprog2", "42:event=open", "42:open.retval>=0", // no events expected + }, + cmdEvents: []cmdEvents{ + newCmdEvents("fakeprog1", + []trace.Event{ + expectEvent(anyHost, "fakeprog1", cpuID, anyPID, 0, events.Openat, orScopes(64), + expectArg("dirfd", int32(0)), + expectArg("flags", int32(0)), + expectArg("mode", uint32(0)), + ), + }, + []string{}, + ), + newCmdEvents("fakeprog2", + []trace.Event{}, // no events expected + []string{}, + ), + }, + useSyscaller: true, + test: ExpectAllInOrder, + }, + { + name: "comm: trace events set in a specific scope from ls command", + filterArgs: []string{ + "64:comm=ls", + }, + cmdEvents: []cmdEvents{ + newCmdEvents("ls", []trace.Event{ + expectEvent(anyHost, "ls", cpuID, anyPID, 0, anyEventID, orScopes(64)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllEqualTo, + }, + { + name: "comm: trace events set in a specific scope from ls command", + filterArgs: []string{ + "64:comm=ls", + "42:comm=who", + }, + cmdEvents: []cmdEvents{ + newCmdEvents("ls", []trace.Event{ + expectEvent(anyHost, "ls", cpuID, anyPID, 0, anyEventID, orScopes(64)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllEqualTo, + }, + { + name: "comm: trace events set in a specific scope from ls and who commands", + filterArgs: []string{ + "64:comm=ls", + "42:comm=who", + }, + cmdEvents: []cmdEvents{ + newCmdEvents("ls", []trace.Event{ + expectEvent(anyHost, "ls", cpuID, anyPID, 0, anyEventID, orScopes(64)), + }, + []string{}, + ), + newCmdEvents("who", []trace.Event{ + expectEvent(anyHost, "who", cpuID, anyPID, 0, anyEventID, orScopes(42)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllEqualTo, + }, + { + name: "event: args: context: only security_file_open from \"execve\" syscall", + filterArgs: []string{"42:event=security_file_open", "42:security_file_open.context.syscall=execve", "42:security_file_open.args.pathname=*ls"}, + cmdEvents: []cmdEvents{ + newCmdEvents("bash -c ls", + []trace.Event{ + expectEvent(anyHost, "bash", // note that comm name is from the runner + cpuID, anyPID, 0, events.SecurityFileOpen, orScopes(42), expectArg("pathname", "*ls")), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllEqualTo, + }, + { + name: "comm: event: do a file write", + filterArgs: []string{"42:comm=tee", "42:event=magic_write"}, + cmdEvents: []cmdEvents{ + newCmdEvents("bash -c '/usr/bin/tee /tmp/magic_write_test < <(echo 42)'", + []trace.Event{ + expectEvent(anyHost, "tee", cpuID, anyPID, 0, events.MagicWrite, orScopes(42)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllInOrder, + }, + + // TODO: add tests using signature events + // This is currently not possible since signature events are dynamically + // created and an event like anti_debugging is not known in advance. + // { + // name: "comm: event: sign: trace sys events + signature events in separte scopes", + // filterArgs: []string{ + // "3:comm=ping", "3:event=net_packet_icmp", + // "5:event=ptrace", "5:ptrace.args.pid=0", + // "9:event=anti_debugging", + // }, + // cmdEvents: []cmdEvents{ + // newCmdEvents("ping -c1 0.0.0.0", + // []trace.Event{ + // expectEvent(anyHost, "ping", cpuID, anyPID, 0, events.NetPacketICMP, orScopes(3)), + // }, + // []string{}, + // ), + // newCmdEvents("strace ls", + // []trace.Event{ + // expectEvent(anyHost, "strace", cpuID, anyPID, 0, events.Ptrace, orScopes(5)), + // expectEvent(anyHost, "strace", cpuID, anyPID, 0, events.anti_debugging, orScopes(9)), + // }, + // []string{}, + // ), + // }, + // useSyscaller: false, + // test: ExpectAtLeastOneOfEach, + // }, + + // events matched in multiple scopes - intertwined workloads + { + name: "comm: event: trace events from ping command in multiple scopes", + filterArgs: []string{ + "3:comm=ping", "3:event=net_packet_icmp", + "5:comm=ping", "5:event=net_packet_icmp", + }, + cmdEvents: []cmdEvents{ + newCmdEvents("ping -c1 0.0.0.0", + []trace.Event{ + expectEvent(anyHost, "ping", cpuID, anyPID, 0, events.NetPacketICMP, orScopes(3, 5)), + expectEvent(anyHost, "ping", cpuID, anyPID, 0, events.NetPacketICMP, orScopes(3, 5)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllInOrder, + }, + { + name: "comm: event: trace events from ping command in multiple scopes", + filterArgs: []string{ + "3:comm=ping", "3:event=net_packet_icmp", + "5:comm=ping", "5:event=net_packet_icmp,setuid", + }, + cmdEvents: []cmdEvents{ + newCmdEvents("ping -c1 0.0.0.0", + []trace.Event{ + expectEvent(anyHost, "ping", cpuID, anyPID, 0, events.Setuid, orScopes(5)), + + expectEvent(anyHost, "ping", cpuID, anyPID, 0, events.NetPacketICMP, orScopes(3, 5)), + expectEvent(anyHost, "ping", cpuID, anyPID, 0, events.NetPacketICMP, orScopes(3, 5)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllInOrder, + }, + { + name: "comm: event: trace events from ping command in multiple scopes", + filterArgs: []string{ + "3:comm=ping", "3:event=net_packet_icmp", + "5:comm=ping", "5:event=net_packet_icmp", + "7:comm=ping", "7:event=sched_process_exec", + "9:comm=ping", "9:event=sched_process_exec,security_socket_connect", + }, + cmdEvents: []cmdEvents{ + newCmdEvents("ping -c1 0.0.0.0", + []trace.Event{ + expectEvent(anyHost, "ping", cpuID, anyPID, 0, events.SchedProcessExec, orScopes(7, 9)), + + expectEvent(anyHost, "ping", cpuID, anyPID, 0, events.SecuritySocketConnect, orScopes(9)), + + expectEvent(anyHost, "ping", cpuID, anyPID, 0, events.NetPacketICMP, orScopes(3, 5)), + expectEvent(anyHost, "ping", cpuID, anyPID, 0, events.NetPacketICMP, orScopes(3, 5)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllInOrder, + }, + { + name: "comm: trace only events from from ls and who commands in multiple scopes", + filterArgs: []string{ + "64:comm=ls", + "42:comm=who,ls", + }, + cmdEvents: []cmdEvents{ + newCmdEvents("ls", + []trace.Event{ + expectEvent(anyHost, "ls", cpuID, anyPID, 0, anyEventID, orScopes(64, 42)), + }, + []string{}, + ), + newCmdEvents("who", + []trace.Event{ + expectEvent(anyHost, "who", cpuID, anyPID, 0, anyEventID, orScopes(42)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllEqualTo, + }, + { + name: "comm: trace at least one event in multiple scopes from ls and who commands", + filterArgs: []string{ + "64:comm=ls", + "42:comm=who,ls", + }, + cmdEvents: []cmdEvents{ + newCmdEvents("ls", + []trace.Event{ + expectEvent(anyHost, "ls", cpuID, anyPID, 0, anyEventID, orScopes(64, 42)), + }, + []string{}, + ), + newCmdEvents("who", + []trace.Event{ + expectEvent(anyHost, "who", cpuID, anyPID, 0, anyEventID, orScopes(42)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAtLeastOneOfEach, + }, + } + + runtime.GOMAXPROCS(1) // make sure we don't have more than one thread + + // run tests cases + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + // set up cpu affinity and lock thread + setCPU() + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + ctx, cancel := context.WithCancel(context.Background()) + filterScopes, err := flags.PrepareFilterScopes(tc.filterArgs) + require.NoError(t, err) + + eventChan := make(chan trace.Event, 2000) + config := tracee.Config{ + FilterScopes: filterScopes, + ChanEvents: eventChan, + Capabilities: &tracee.CapabilitiesConfig{ + BypassCaps: true, + }, + } + + buf := &eventBuffer{} + + go func() { + for evt := range eventChan { + buf.mu.Lock() + buf.events = append(buf.events, evt) + buf.mu.Unlock() + } + }() + + trc := startTracee(t, config, nil, nil, ctx) + + waitforTraceeStart(t, trc, time.Now()) + + tc.test(t, tc.cmdEvents, buf, tc.useSyscaller) + + cancel() + buf.clear() + }) + } +} + +// expectArg is a helper function to create a trace.Argument with the name and value fields set +// If value has a star as wildcard, the value must be passed as it is due to test functions logic +func expectArg(name string, value interface{}) trace.Argument { + return trace.Argument{ + ArgMeta: trace.ArgMeta{ + Name: name, + }, + Value: value, + } +} + +// orScopes is a helper function to create a bit mask of the given scopes +func orScopes(scopes ...uint) uint64 { + var res uint64 + + for _, scope := range scopes { + utils.SetBit(&res, scope-1) + } + + return res +} + +// cmdEvents is a struct to hold the command to run +// and the expected events and sets +type cmdEvents struct { + runCmd string + evts []trace.Event + sets []string +} + +// newCmdEvents is a helper function to create a cmdEvents +func newCmdEvents(runCmd string, evts []trace.Event, sets []string) cmdEvents { + return cmdEvents{ + runCmd: runCmd, + evts: evts, + sets: sets, + } +} + +const ( + anyProcessorID = -1 + anyHost = "" + anyComm = "" + anyEventID = -1 + anyPID = -1 + anyUID = -1 + anyScope = 0 +) + +// expectEvent is a helper function to create a trace.Event +func expectEvent(host, comm string, processorID, pid, uid int, eventID events.ID, scopes uint64, args ...trace.Argument) trace.Event { + return trace.Event{ + ProcessorID: processorID, + ProcessID: pid, + UserID: uid, + ProcessName: comm, + HostName: host, + EventID: int(eventID), + MatchedScopes: scopes, + Args: args, + } +} + +// proc represents a process, with its pid and the number of events it should generate +type proc struct { + pid int + expectedEvts int +} + +// runCmd runs a command and returns a process +func runCmd(t *testing.T, ce cmdEvents, actual *eventBuffer, useSyscaller, failOnTimeout bool) proc { + var ( + pid int + err error + forkTimeout = 1 * time.Second + ) + + if useSyscaller { + pid, err = forkAndCallSys(ce.runCmd, ce.evts, forkTimeout) + } else { + pid, err = forkAndExec(ce.runCmd, forkTimeout) + } + require.NoError(t, err) + + waitForTraceeOutputEvents(t, actual, time.Now(), len(ce.evts), failOnTimeout) + + return proc{ + pid: pid, + expectedEvts: len(ce.evts), + } +} + +// runCmds runs a list of commands and returns a list of processes +// It also returns the number of expected events from all processes +func runCmds(t *testing.T, cmdEvents []cmdEvents, actual *eventBuffer, useSyscaller, failOnTimeout bool) ([]proc, int) { + var ( + procs = make([]proc, 0) + expectedEvts int + ) + + for _, cmd := range cmdEvents { + var ( + pid int + err error + forkTimeout = 1 * time.Second + ) + + if useSyscaller { + pid, err = forkAndCallSys(cmd.runCmd, cmd.evts, forkTimeout) + } else { + pid, err = forkAndExec(cmd.runCmd, forkTimeout) + } + require.NoError(t, err) + + procs = append(procs, proc{pid, len(cmd.evts)}) + expectedEvts += len(cmd.evts) + } + + waitForTraceeOutputEvents(t, actual, time.Now(), expectedEvts, failOnTimeout) + + return procs, expectedEvts +} + +// isInSets checks if a syscall is in a set of syscalls +func isInSets(syscallName string, sets []string) bool { + for _, set := range sets { + if syscallName == set { + return true + } + } + + return false +} + +// getAllSyscallsInSet returns all syscalls in given set +func getAllSyscallsInSet(set string) []string { + var syscallsInSet []string + + for _, v := range events.Definitions.Events() { + for _, c := range v.Sets { + if c == set { + syscallsInSet = append(syscallsInSet, v.Name) + } + } + } + + return syscallsInSet +} + +// getAllSyscallsInSets returns all syscalls in given sets +func getAllSyscallsInSets(sets []string) []string { + var syscallsInSet []string + + for _, set := range sets { + syscallsInSet = append(syscallsInSet, getAllSyscallsInSet(set)...) + } + + return syscallsInSet +} + +// isCmdAShellRunner checks if the command is executed by a shell +func isCmdAShellRunner(cmd string) bool { + if !strings.HasPrefix(cmd, "bash") && !strings.HasPrefix(cmd, "sh") { + return false + } + if !strings.Contains(cmd, "-c") { + return false + } + + return true +} + +// pidToCheck returns the pid of the process to check for events +func pidToCheck(cmd string, actEvt trace.Event) int { + if isCmdAShellRunner(cmd) { + return actEvt.ParentProcessID + } + + return actEvt.ProcessID +} + +// ExpectAtLeastOneOfEach validates that at least one event from each command was captured +func ExpectAtLeastOneOfEach(t *testing.T, cmdEvents []cmdEvents, actual *eventBuffer, useSyscaller bool) { + for _, exp := range cmdEvents { + syscallsInSets := []string{} + checkSets := len(exp.sets) > 0 + if checkSets { + syscallsInSets = getAllSyscallsInSets(exp.sets) + } + + actual.clear() + // first stage: run commands + proc := runCmd(t, exp, actual, useSyscaller, true) + if len(exp.evts) == 0 && proc.expectedEvts > 0 { + t.Fatalf("expected no events for command %s, but got %d", exp.runCmd, proc.expectedEvts) + } + + // second stage: validate events + actual.mu.RLock() + for _, expEvt := range exp.evts { + found := false + checkHost := expEvt.HostName != anyHost + checkComm := expEvt.ProcessName != anyComm + checkProcessorID := expEvt.ProcessorID != anyProcessorID + checkPID := expEvt.ProcessID != anyPID + checkUID := expEvt.UserID != anyUID + checkEventID := expEvt.EventID != anyEventID + checkScope := expEvt.MatchedScopes != anyScope + + if len(exp.evts) > 0 && proc.expectedEvts == 0 { + t.Fatalf("expected events for command %s, but got none", exp.runCmd) + } + + for _, actEvt := range actual.events { + if checkSets && !isInSets(actEvt.EventName, syscallsInSets) { + continue + } + + if checkHost && actEvt.HostName != expEvt.HostName { + continue + } + if checkComm && actEvt.ProcessName != expEvt.ProcessName { + continue + } + if checkProcessorID && actEvt.ProcessorID != expEvt.ProcessorID { + continue + } + if checkPID && pidToCheck(exp.runCmd, actEvt) != expEvt.ProcessID { + continue + } + if checkPID && actEvt.ProcessID != expEvt.ProcessID { + continue + } + if checkUID && actEvt.UserID != expEvt.UserID { + continue + } + if checkEventID && actEvt.EventID != expEvt.EventID { + continue + } + if checkScope && actEvt.MatchedScopes != expEvt.MatchedScopes { + continue + } + + // check args + for _, expArg := range expEvt.Args { + actArg, err := helpers.GetTraceeArgumentByName(actEvt, expArg.Name, helpers.GetArgOps{DefaultArgs: false}) + require.NoError(t, err) + switch v := expArg.Value.(type) { + case string: + actVal := actArg.Value.(string) + if strings.Contains(v, "*") { + v = strings.ReplaceAll(v, "*", "") + if !strings.Contains(actVal, v) { + continue + } + } else { + if !assert.ObjectsAreEqual(v, actVal) { + continue + } + } + default: + if !assert.ObjectsAreEqual(v, actArg.Value) { + continue + } + } + } + + // if we got here, it means we found a match and can stop searching + found = true + break + } + // evaluate found + require.True(t, found, "Event %+v:\nnot found in actual output:\n%+v", expEvt, actual.events) + } + actual.mu.RUnlock() + } +} + +// ExpectAllEqualTo expects all events to be equal to the expected events +func ExpectAllEqualTo(t *testing.T, cmdEvents []cmdEvents, actual *eventBuffer, useSyscaller bool) { + for _, exp := range cmdEvents { + if len(exp.evts) != 1 { + t.Fatalf("ExpectAllEqualTo test requires exactly one event per command") + } + + actual.clear() + // first stage: run commands + proc := runCmd(t, exp, actual, useSyscaller, true) + + if proc.expectedEvts == 0 { + t.Fatalf("expected one event for command %s, but got none", exp.runCmd) + } + syscallsInSets := []string{} + checkSets := len(exp.sets) > 0 + if checkSets { + syscallsInSets = getAllSyscallsInSets(exp.sets) + } + + // second stage: validate events + actual.mu.RLock() + for _, expEvt := range exp.evts { + checkHost := expEvt.HostName != anyHost + checkComm := expEvt.ProcessName != anyComm + checkProcessorID := expEvt.ProcessorID != anyProcessorID + checkPID := expEvt.ProcessID != anyPID + checkUID := expEvt.UserID != anyUID + checkEventID := expEvt.EventID != anyEventID + checkScope := expEvt.MatchedScopes != anyScope + + for _, actEvt := range actual.events { + if checkSets { + assert.Contains(t, syscallsInSets, actEvt.EventName, "event name in set") + } + + if checkHost { + assert.Equal(t, expEvt.HostName, actEvt.HostName, "host name") + } + if checkComm { + assert.Equal(t, expEvt.ProcessName, actEvt.ProcessName, "comm") + } + if checkProcessorID { + assert.Equal(t, expEvt.ProcessorID, actEvt.ProcessorID, "processor id") + } + if checkPID { + assert.Equal(t, expEvt.ProcessID, pidToCheck(exp.runCmd, actEvt), "pid") + } + if checkUID { + assert.Equal(t, expEvt.UserID, actEvt.UserID, "user id") + } + if checkEventID { + assert.Equal(t, expEvt.EventID, actEvt.EventID, "event id") + } + if checkScope { + assert.Equal(t, expEvt.MatchedScopes, actEvt.MatchedScopes, "matched scopes") + } + + // check args + for _, expArg := range expEvt.Args { + actArg, err := helpers.GetTraceeArgumentByName(actEvt, expArg.Name, helpers.GetArgOps{DefaultArgs: false}) + require.NoError(t, err) + switch v := expArg.Value.(type) { + case string: + actVal := actArg.Value.(string) + if strings.Contains(v, "*") { + v = strings.ReplaceAll(v, "*", "") + assert.Contains(t, actVal, v, "arg value") + } else { + assert.Equal(t, v, actVal, "arg value") + } + default: + assert.Equal(t, v, actArg.Value, "arg value") + } + } + } + } + actual.mu.RUnlock() + } +} + +// ExpectAllInOrder expects all events to be equal to the expected events in the same order +func ExpectAllInOrder(t *testing.T, cmdEvents []cmdEvents, actual *eventBuffer, useSyscaller bool) { + // first stage: run commands + actual.clear() + procs, _ := runCmds(t, cmdEvents, actual, useSyscaller, true) + + if len(procs) > len(cmdEvents) { + t.Fatalf("expected %d commands, but got %d", len(cmdEvents), len(procs)) + } + + // second stage: check events + actual.mu.RLock() + for cmdIdx, exp := range cmdEvents { + syscallsInSets := []string{} + checkSets := len(exp.sets) > 0 + if checkSets { + syscallsInSets = getAllSyscallsInSets(exp.sets) + } + + // compare the expected events with the actual events in the same order + for evtIdx, expEvt := range exp.evts { + actEvt := actual.events[cmdIdx*len(exp.evts)+evtIdx] + + if checkSets { + assert.Contains(t, syscallsInSets, actEvt.EventName, "event name in set") + } + checkHost := expEvt.HostName != anyHost + checkComm := expEvt.ProcessName != anyComm + checkProcessorID := expEvt.ProcessorID != anyProcessorID + checkPID := expEvt.ProcessID != anyPID + checkUID := expEvt.UserID != anyUID + checkEventID := expEvt.EventID != anyEventID + checkScope := expEvt.MatchedScopes != anyScope + + if checkHost { + assert.Equal(t, expEvt.HostName, actEvt.HostName, "host name") + } + if checkComm { + assert.Equal(t, expEvt.ProcessName, actEvt.ProcessName, "comm") + } + if checkProcessorID { + assert.Equal(t, expEvt.ProcessorID, actEvt.ProcessorID, "processor id") + } + if checkPID { + assert.Equal(t, expEvt.ProcessID, pidToCheck(exp.runCmd, actEvt), "pid") + } + if checkUID { + assert.Equal(t, expEvt.UserID, actEvt.UserID, "user id") + } + if checkEventID { + assert.Equal(t, expEvt.EventID, actEvt.EventID, "event id") + } + if checkScope { + assert.Equal(t, expEvt.MatchedScopes, actEvt.MatchedScopes, "matched scopes") + } + + // check args + for _, expArg := range expEvt.Args { + actArg, err := helpers.GetTraceeArgumentByName(actEvt, expArg.Name, helpers.GetArgOps{DefaultArgs: false}) + require.NoError(t, err) + switch v := expArg.Value.(type) { + case string: + if strings.Contains(v, "*") { + v = strings.ReplaceAll(v, "*", "") + assert.Contains(t, actArg.Value, v, "arg value") + } else { + assert.Equal(t, v, actArg.Value, "arg value") + } + + default: + assert.Equal(t, v, actArg.Value, "arg value") + } + } + } + } + actual.mu.RUnlock() +} diff --git a/tests/integration/exec.go b/tests/integration/exec.go new file mode 100644 index 000000000000..1e5004b827cb --- /dev/null +++ b/tests/integration/exec.go @@ -0,0 +1,190 @@ +package integration + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "syscall" + "time" + + "github.com/aquasecurity/tracee/pkg/events" + "github.com/aquasecurity/tracee/types/trace" + "golang.org/x/sys/unix" +) + +const cpuID = 0 // CPU to pin the process to + +// setCPU pins the current process to a specific CPU +func setCPU() { + cpuMask := unix.CPUSet{} + cpuMask.Set(cpuID) + unix.SchedSetaffinity(0, &cpuMask) +} + +// parseCmd parses a command string into a command and arguments +func parseCmd(fullCmd string) (string, []string, error) { + pattern := `'[^']*'|\S+` // split on spaces, but not spaces inside single quotes + re := regexp.MustCompile(pattern) + 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 and the ones we need for the exec +// 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 + null, err := os.OpenFile("/dev/null", os.O_RDWR, 0) + if err != nil { + return fmt.Errorf("open /dev/null: %w", err) + } + err = syscall.Dup2(int(null.Fd()), unix.Stdout) + if err != nil { + return fmt.Errorf("dup2: %w", err) + } + err = syscall.Close(int(null.Fd())) + if err != nil { + return fmt.Errorf("close: %w", err) + } + + return nil +} + +// waitChild waits for a child process to exit +func waitChild(pid int, timeout time.Duration) (int, error) { + var ( + deadline = time.Now().Add(1 * time.Second) + wopts = syscall.WNOHANG | syscall.WUNTRACED | syscall.WCONTINUED + wstat syscall.WaitStatus + ) + + for { + pid, err := syscall.Wait4(int(pid), &wstat, wopts, nil) + if err != nil { + return -1, err + } + + if pid != 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 + if time.Now().After(deadline) { + syscall.Kill(-pid, syscall.SIGTERM) + return -1, fmt.Errorf("timed out waiting for child process to exit") + } + + time.Sleep(100 * time.Millisecond) // poll every 100ms + } +} + +// forkAndExec forks and execs a command +// It returns the pid of the child process +func forkAndExec(cmd string, timeout time.Duration) (int, error) { + pid, _, errno := syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0) + if pid == 0 { // child + err := handleFDs() + if err != nil { + fmt.Fprintf(os.Stderr, "handleFDs: %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) { + pid, _, errno := syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0) + if pid == 0 { // child + err := handleFDs() + if err != nil { + fmt.Fprintf(os.Stderr, "handleFDs: %v", err) + syscall.Exit(1) + } + + evtsID := make([]events.ID, 0) + eventsDefinitions := events.Definitions + fmt.Fprintf(os.Stderr, "\tabout to call 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) +} diff --git a/tests/integration/exec_test.go b/tests/integration/exec_test.go new file mode 100644 index 000000000000..2696f16164b0 --- /dev/null +++ b/tests/integration/exec_test.go @@ -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) + } + } +} diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index 3c37fd059a39..5c0692c4bbab 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -1,27 +1,14 @@ package integration import ( - "context" _ "embed" - "fmt" - "io" - "os" - "strconv" - "strings" - "syscall" "testing" - "time" - "github.com/aquasecurity/tracee/pkg/cmd/flags" - tracee "github.com/aquasecurity/tracee/pkg/ebpf" "github.com/aquasecurity/tracee/pkg/events" - "github.com/aquasecurity/tracee/signatures/helpers" - "github.com/aquasecurity/tracee/types/trace" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) -func TestInitNamespacesEvent(t *testing.T) { +func Test_InitNamespacesEvent(t *testing.T) { procNamespaces := [...]string{"mnt", "cgroup", "pid", "pid_for_children", "time", "time_for_children", "user", "ipc", "net", "uts"} evts := events.InitNamespacesEvent() initNamespaces := make(map[string]uint32) @@ -37,429 +24,3 @@ func TestInitNamespacesEvent(t *testing.T) { assert.NotZero(t, initNamespaces[namespace]) } } - -// small set of actions to trigger a magic write event -func checkMagicwrite(t *testing.T, gotOutput *[]trace.Event) { - - _, err := forkAndExecFunction(doMagicWrite) - require.NoError(t, err) - - waitForTraceeOutput(t, gotOutput, time.Now(), true) - - // check tracee output - for _, evt := range *gotOutput { - assert.Equal(t, []byte(evt.EventName), []byte("magic_write")) - } -} - -// execute a ls command -func checkExeccommand(t *testing.T, gotOutput *[]trace.Event) { - _, err := forkAndExecFunction(doLs) - require.NoError(t, err) - - waitForTraceeOutput(t, gotOutput, time.Now(), true) - - // check tracee output - processNames := []string{} - for _, evt := range *gotOutput { - processNames = append(processNames, evt.ProcessName) - } - for _, pname := range processNames { - assert.Equal(t, "ls", pname) - } -} - -// only capture new pids after tracee -func checkPidnew(t *testing.T, gotOutput *[]trace.Event) { - traceePid := os.Getpid() - - _, err := forkAndExecFunction(doLs) - require.NoError(t, err) - - waitForTraceeOutput(t, gotOutput, time.Now(), true) - - // output should only have events with pids greater (newer) than tracee - pids := []int{} - for _, evt := range *gotOutput { - if evt.ProcessName == "ls" { - pids = append(pids, evt.ProcessID) - } - } - for _, pid := range pids { - assert.Greater(t, pid, traceePid) - } -} - -// only capture uids of 0 that are run by comm ls -func checkUidZero(t *testing.T, gotOutput *[]trace.Event) { - _, err := forkAndExecFunction(doLs) - require.NoError(t, err) - - waitForTraceeOutput(t, gotOutput, time.Now(), true) - - // check output length - require.NotEmpty(t, gotOutput) - - // output should only have events with uids of 0 - uids := []int{} - for _, evt := range *gotOutput { - uids = append(uids, evt.UserID) - } - for _, uid := range uids { - require.Zero(t, uid) - } -} - -// trigger ls from uid 0 (tests run as root) and check if empty -func checkUidNonZero(t *testing.T, gotOutput *[]trace.Event) { - _, err := forkAndExecFunction(doLs) - require.NoError(t, err) - - waitForTraceeOutput(t, gotOutput, time.Now(), false) - - // check output length - assert.Empty(t, gotOutput) -} - -// check that execve event is called -func checkExecve(t *testing.T, gotOutput *[]trace.Event) { - _, err := forkAndExecFunction(doLs) - require.NoError(t, err) - - waitForTraceeOutput(t, gotOutput, time.Now(), true) - - // check output length - require.NotEmpty(t, gotOutput) - - // output should only have events with event name of execve - eventNames := []string{} - for _, evt := range *gotOutput { - eventNames = append(eventNames, evt.EventName) - } - for _, en := range eventNames { - if len(en) > 0 { - require.Equal(t, "execve", en) - } - } -} - -// check for filesystem set when ls is invoked -func checkSetFs(t *testing.T, gotOutput *[]trace.Event) { - _, err := forkAndExecFunction(doLs) - require.NoError(t, err) - - waitForTraceeOutput(t, gotOutput, time.Now(), true) - - // check output length - require.NotEmpty(t, gotOutput) - - expectedSyscalls := getAllSyscallsInSet("fs") - - // output should only have events with events in the set of filesystem syscalls - eventNames := []string{} - for _, evt := range *gotOutput { - eventNames = append(eventNames, evt.EventName) - } - for _, en := range eventNames { - require.Contains(t, expectedSyscalls, en) - } -} - -func checkNewContainers(t *testing.T, gotOutput *[]trace.Event) { - containerIdBytes, err := forkAndExecFunction(doDockerRun) - require.NoError(t, err) - containerId := strings.TrimSuffix(string(containerIdBytes), "\n") - containerIds := []string{} - for _, evt := range *gotOutput { - containerIds = append(containerIds, evt.ContainerID) - } - for _, id := range containerIds { - assert.Equal(t, containerId, id) - } -} - -func getAllSyscallsInSet(set string) []string { - var syscallsInSet []string - for _, v := range events.Definitions.Events() { - for _, c := range v.Sets { - if c == set { - syscallsInSet = append(syscallsInSet, v.Name) - } - } - } - return syscallsInSet -} - -func checkSecurityFileOpenExecve(t *testing.T, gotOutput *[]trace.Event) { - _, err := forkAndExecFunction(doFileOpen) - require.NoError(t, err) - - for _, evt := range *gotOutput { - assert.Equal(t, "execve", evt.Syscall) - } -} - -func checkScope42SecurityFileOpenLs(t *testing.T, gotOutput *[]trace.Event) { - _, err := forkAndExecFunction(doLs) - require.NoError(t, err) - - waitForTraceeOutput(t, gotOutput, time.Now(), true) - - for _, evt := range *gotOutput { - // ls - scope 42 - assert.Equal(t, "ls", evt.ProcessName) - assert.Equal(t, uint64(1<<41), evt.MatchedScopes) - arg, err := helpers.GetTraceeArgumentByName(evt, "pathname", helpers.GetArgOps{DefaultArgs: false}) - require.NoError(t, err) - assert.Contains(t, arg.Value, "integration") - } -} - -// checkExecveOnScopes4And2 demands an ordered events submission -func checkExecveOnScopes4And2(t *testing.T, gotOutput *[]trace.Event) { - _, err := forkAndExecFunction(doLsUname) - require.NoError(t, err) - - waitForTraceeOutput(t, gotOutput, time.Now(), true) - - // check output length - require.Len(t, *gotOutput, 2) - var evts [2]trace.Event - - // output should only have events with event name of execve - for i, evt := range *gotOutput { - assert.Equal(t, "sched_process_exit", evt.EventName) - evts[i] = evt - } - - // ls - scope 4 - assert.Equal(t, evts[0].ProcessName, "ls") - assert.Equal(t, uint64(1<<3), evts[0].MatchedScopes, "MatchedScopes") - - // uname - scope 2 - assert.Equal(t, evts[1].ProcessName, "uname") - assert.Equal(t, uint64(1<<1), evts[1].MatchedScopes, "MatchedScopes") -} - -func checkDockerdBinaryFilter(t *testing.T, gotOutput *[]trace.Event) { - dockerdPidBytes, err := forkAndExecFunction(getDockerdPid) - require.NoError(t, err) - dockerdPidString := strings.TrimSuffix(string(dockerdPidBytes), "\n") - dockerdPid, err := strconv.ParseInt(dockerdPidString, 10, 64) - require.NoError(t, err) - _, err = forkAndExecFunction(doDockerRun) - require.NoError(t, err) - - waitForTraceeOutput(t, gotOutput, time.Now(), true) - - processIds := []int{} - for _, evt := range *gotOutput { - processIds = append(processIds, evt.ProcessID) - } - assert.Contains(t, processIds, int(dockerdPid)) -} - -func checkLsAndWhichBinaryFilterWithScopes(t *testing.T, gotOutput *[]trace.Event) { - var err error - _, err = forkAndExecFunction(doLs) - require.NoError(t, err) - _, err = forkAndExecFunction(doWhichLs) - require.NoError(t, err) - - waitForTraceeOutput(t, gotOutput, time.Now(), true) - - for _, evt := range *gotOutput { - procName := evt.ProcessName - if procName != "ls" && procName != "which" { - t.Fail() - } - } -} - -func Test_EventFilters(t *testing.T) { - testCases := []struct { - name string - filterArgs []string - eventFunc func(*testing.T, *[]trace.Event) - }{ - { - name: "do a file write", - filterArgs: []string{"event=magic_write"}, - eventFunc: checkMagicwrite, - }, - { - name: "execute a command", - filterArgs: []string{"comm=ls"}, - eventFunc: checkExeccommand, - }, - { - name: "trace new pids", - filterArgs: []string{"pid=new"}, - eventFunc: checkPidnew, - }, - { - name: "trace uid 0 with comm ls", - filterArgs: []string{"uid=0", "comm=ls"}, - eventFunc: checkUidZero, - }, - { - name: "trace only ls comms from uid>0 (should be empty)", - filterArgs: []string{"uid>0", "comm=ls"}, - eventFunc: checkUidNonZero, - }, - //TODO: Add pid=0,1 - //TODO: Add pid=0 pid=1 - //TODO: Add uid>0 - //TODO: Add pid>0 pid<1000 - //TODO: Add u>0 u!=1000 - { - name: "trace filesystem events from comm ls", - filterArgs: []string{"s=fs", "comm=ls"}, - eventFunc: checkSetFs, - }, - { - name: "trace only execve events from comm ls", - filterArgs: []string{"event=execve", "execve.args.pathname=*ls"}, - eventFunc: checkExecve, - }, - { - name: "trace only execve events that starts with /usr/bin", - filterArgs: []string{"event=execve", "execve.args.pathname=/usr/bin*"}, - eventFunc: checkExecve, - }, - { - name: "trace only execve events that contains l", - filterArgs: []string{"event=execve", "execve.args.pathname=*l*"}, - eventFunc: checkExecve, - }, - { - name: "trace only events from new containers", - filterArgs: []string{"container=new", "event!=container_create,container_remove"}, - eventFunc: checkNewContainers, - }, - { - name: "trace event set in a specific scope", - filterArgs: []string{"42:comm=ls", "42:event=security_file_open", "42:security_file_open.args.pathname=*integration"}, - eventFunc: checkScope42SecurityFileOpenLs, - }, - { - name: "trace events set in two specific scope", - filterArgs: []string{ - "4:event=sched_process_exit", "4:comm=ls", - "2:event=sched_process_exit", "2:comm=uname", - }, - eventFunc: checkExecveOnScopes4And2, - }, - { - name: "trace only security_file_open from \"execve\" syscall", - filterArgs: []string{"event=security_file_open", "security_file_open.context.syscall=execve"}, - eventFunc: checkSecurityFileOpenExecve, - }, - { - name: "trace only events from \"/usr/bin/dockerd\" binary and contain it's pid", - filterArgs: []string{"bin=/usr/bin/dockerd"}, - eventFunc: checkDockerdBinaryFilter, - }, - { - name: "trace events from ls and which binary in separate scopes", - filterArgs: []string{"1:bin=/usr/bin/ls", "2:bin=/usr/bin/which"}, - eventFunc: checkLsAndWhichBinaryFilterWithScopes, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - filterScopes, err := flags.PrepareFilterScopes(tc.filterArgs) - require.NoError(t, err) - - eventChan := make(chan trace.Event, 1000) - config := tracee.Config{ - ChanEvents: eventChan, - Capabilities: &tracee.CapabilitiesConfig{ - BypassCaps: true, - }, - } - config.FilterScopes = filterScopes - eventOutput := []trace.Event{} - - go func() { - for evt := range eventChan { - eventOutput = append(eventOutput, evt) - } - }() - - trc := startTracee(t, config, nil, nil, ctx) - - waitforTraceeStart(t, trc, time.Now()) - - tc.eventFunc(t, &eventOutput) - - cancel() - }) - } -} - -type testFunc string - -const ( - doMagicWrite testFunc = "do_magic_write" - doLs testFunc = "do_ls" - doLsUname testFunc = "do_ls_uname" - doDockerRun testFunc = "do_docker_run" - doFileOpen testFunc = "do_file_open" - getDockerdPid testFunc = "get_dockerd_pid" - doWhichLs testFunc = "do_which_ls" -) - -//go:embed tester.sh -var testerscript []byte - -// forkAndExecFunction runs a function in `tester.sh` in it's own system process. -// This is so Tracee running in the current pid can pick the command up. -// It returns the output of the process and a possible error. -func forkAndExecFunction(funcName testFunc) ([]byte, error) { - - f, err := os.CreateTemp("", "tracee-integration-test-script") - if err != nil { - return nil, fmt.Errorf("couldn't create temp file for script: %w", err) - } - - _, err = f.Write(testerscript) - if err != nil { - return nil, fmt.Errorf("couldn't write temp script: %w", err) - } - - err = f.Close() - if err != nil { - return nil, fmt.Errorf("couldn't close fd for script: %w", err) - } - - tmpOutputFile, err := os.CreateTemp("/tmp", "tracee-test*") - if err != nil { - return nil, fmt.Errorf("couldn't create temporary output file: %w", err) - } - - err = os.Chmod(f.Name(), 0777) - if err != nil { - return nil, fmt.Errorf("couldn't chmod script file: %w", err) - } - - _, err = syscall.ForkExec(f.Name(), []string{f.Name(), string(funcName), tmpOutputFile.Name()}, - &syscall.ProcAttr{ - Files: []uintptr{0, 1, 2, tmpOutputFile.Fd()}, - Env: os.Environ(), - }) - if err != nil { - return nil, fmt.Errorf("couldn't fork/exec: %w", err) - } - - // ForkExec doesn't block, wait for output - time.Sleep(time.Second) - - output, err := io.ReadAll(tmpOutputFile) - if err != nil { - return nil, fmt.Errorf("couldn't read output: %w", err) - } - return output, nil -} diff --git a/tests/integration/syscaller.go b/tests/integration/syscaller.go new file mode 100644 index 000000000000..7188fbfae6b2 --- /dev/null +++ b/tests/integration/syscaller.go @@ -0,0 +1,57 @@ +package integration + +import ( + "syscall" + "unsafe" + + "github.com/aquasecurity/tracee/pkg/events" +) + +// sysArgs is a struct containing the arguments to be passed to a syscall +type sysArgs struct { + arg1 uintptr + arg2 uintptr + arg3 uintptr + arg4 uintptr + arg5 uintptr + arg6 uintptr +} + +// syscallMap is a map of syscall numbers to the arguments they should be +// called with. If the syscall number is not found in the map, the syscall +// is called with arguments set to 0 +// Some events.ID are internal to tracee and are not syscall numbers, so they +// need further processing before being passed to the syscall +var syscallMap = map[events.ID]sysArgs{ + events.Read: {0, 0, 0, 0, 0, 0}, +} + +// changeOwnComm changes the comm of the current process to the given string +func changeOwnComm(newComm string) error { + comm, err := syscall.BytePtrFromString(newComm) + if err != nil { + return err + } + _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_NAME, uintptr(unsafe.Pointer(comm)), 0) + if errno != 0 { + return syscall.Errno(errno) + } + + return nil +} + +// callsys calls the given events.IDs as syscalls +func callsys(syscalls []events.ID) []error { + errs := make([]error, 0) + for _, sysNum := range syscalls { + var err syscall.Errno + if s, found := syscallMap[sysNum]; found { + _, _, err = syscall.RawSyscall6(uintptr(sysNum), s.arg1, s.arg2, s.arg3, s.arg4, s.arg5, s.arg6) + } else { + _, _, err = syscall.RawSyscall6(uintptr(sysNum), 0, 0, 0, 0, 0, 0) + } + errs = append(errs, syscall.Errno(err)) + } + + return errs +} diff --git a/tests/integration/syscaller_test.go b/tests/integration/syscaller_test.go new file mode 100644 index 000000000000..3096fa1b58c1 --- /dev/null +++ b/tests/integration/syscaller_test.go @@ -0,0 +1,58 @@ +package integration + +import ( + "os" + "runtime" + "syscall" + "testing" + + "github.com/aquasecurity/tracee/pkg/events" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Test_Callsys tests the callsys function +func Test_Callsys(t *testing.T) { + // SYS_READ and SYS_CLOSE are syscalls that, considering this environment, + // should not return an error when called with zeroed arguments + syscalls := []events.ID{events.Read, events.Close} + errs := callsys(syscalls) + for _, err := range errs { + assert.Equal(t, syscall.Errno(0), err) + } + + // SYS_WRITE is a syscall that, considering this environment, + // should return an error when called with zeroed arguments + syscalls = []events.ID{events.Write} + err := callsys(syscalls)[0] + assert.Equal(t, syscall.Errno(9), err) +} + +// Test_ChangeOwnComm tests the changeOwnComm function +func Test_ChangeOwnComm(t *testing.T) { + setCPU() + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + pid, _, _ := syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0) + if pid == 0 { // child + newComm := "test-comm" + // test changing the comm to a valid string + err := changeOwnComm(newComm) + require.NoError(t, err, "Unexpected error") + + // double check that the comm was changed + curComm, err := os.ReadFile("/proc/self/comm") + require.NoError(t, err, "Readfile failed") + + curComm = curComm[:len(curComm)-1] // remove the trailing newline + assert.Equal(t, newComm, string(curComm), "comm was not changed") + + syscall.Exit(0) + } else { // parent + assert.NotEqual(t, -1, pid, "Fork failed") + + _, err := syscall.Wait4(int(pid), nil, 0, nil) + assert.NoError(t, err, "Wait4 failed") + } +} diff --git a/tests/integration/tester.sh b/tests/integration/tester.sh deleted file mode 100755 index d4df14357ba0..000000000000 --- a/tests/integration/tester.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/sh - -do_magic_write() { - tmpFileName=$1 - echo "AAAAA" > $tmpFileName -} - -do_ls() { - ls > /dev/null -} - -do_which_ls() { - which ls > /dev/nul -} - -do_ls_uname() { - # run on the same core to ensure event order - taskset -c 0 ls; uname -} > /dev/null - -do_docker_run() { - outputFileName=$1 - output=$(docker run -d --rm alpine) - $(echo $output > $outputFileName) -} - -get_dockerd_pid() { - outputFileName=$1 - output=$(pidof dockerd) - $(echo $output > $outputFileName) -} - -do_file_open() { - cat /proc/self/comm -} - -# $1 is the function to call -# $2 is the temp file to optionally output to -$1 $2 diff --git a/tests/integration/tracee.go b/tests/integration/tracee.go index 86ebcd090bc1..444ebe83199c 100644 --- a/tests/integration/tracee.go +++ b/tests/integration/tracee.go @@ -3,6 +3,7 @@ package integration import ( "context" "path/filepath" + "sync" "testing" "time" @@ -13,6 +14,28 @@ import ( "github.com/stretchr/testify/require" ) +// eventBuffer is a thread-safe buffer for tracee events +type eventBuffer struct { + mu sync.RWMutex + events []trace.Event +} + +// clear clears the buffer +func (b *eventBuffer) clear() { + b.mu.Lock() + defer b.mu.Unlock() + + b.events = b.events[:0] +} + +// len returns the number of events in the buffer +func (b *eventBuffer) len() int { + b.mu.RLock() + defer b.mu.RUnlock() + + return len(b.events) +} + // load tracee into memory with args func startTracee(t *testing.T, config tracee.Config, output *tracee.OutputConfig, capture *tracee.CaptureConfig, ctx context.Context) *tracee.Tracee { kernelConfig, err := initialize.KernelConfig() @@ -70,32 +93,32 @@ func prepareCapture() *tracee.CaptureConfig { } } -// wait for tracee buffer to fill or timeout to occur, whichever comes first -func waitForTraceeOutput(t *testing.T, gotOutput *[]trace.Event, now time.Time, failOnTimeout bool) { - const CheckTimeout = 5 * time.Second +func waitforTraceeStart(t *testing.T, trc *tracee.Tracee, now time.Time) { + const CheckTimeout = 10 * time.Second for { - if len(*gotOutput) > 0 { + if trc.Running() { break } if time.Since(now) > CheckTimeout { - if failOnTimeout { - t.Logf("timed out on output\n") - t.FailNow() - } - break + t.Logf("timed out on running tracee\n") + t.FailNow() } } } -func waitforTraceeStart(t *testing.T, trc *tracee.Tracee, now time.Time) { - const CheckTimeout = 10 * time.Second +// wait for tracee buffer to fill up with expected number of events (or timeout) +func waitForTraceeOutputEvents(t *testing.T, actual *eventBuffer, now time.Time, expectedEvts int, failOnTimeout bool) { + const CheckTimeout = 5 * time.Second for { - if trc.Running() { + if actual.len() >= expectedEvts { break } if time.Since(now) > CheckTimeout { - t.Logf("timed out on running tracee\n") - t.FailNow() + if failOnTimeout { + t.Logf("timed out on output\n") + t.FailNow() + } + break } } }