From 998cedd3505676a721964156c9d70eaf69a5a820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Geyslan=20Greg=C3=B3rio?= Date: Tue, 7 Feb 2023 18:55:48 -0300 Subject: [PATCH] chore: make integration tests more robust - Add generic functions to check received events in different manners: - ExpectAllInOrder - ExpectAllEqualTo - ExpectAtLeastOneOfEach - ExpectAnyOfEach - Add waitForTraceeOutputEvents function to wait for tracee output events until buffer fills with certain number of events or timeout occurs. - Add waitforTraceeStop function to wait for tracee to stop or until timeout occurs. - Remove tester.sh concentrating all event filter tests in event_filters_test.go. - Create syscaller tool to easily generate syscalls from the command line (strictly for integration tests). It reuses the tracee events package. - Fix goroutine leaks, deadlocks and race conditions. --- .gitignore | 1 + Makefile | 16 + go.mod | 1 + go.sum | 2 + tests/integration/cpu/cpu.go | 18 + tests/integration/event_filters_test.go | 1712 +++++++++++++++++ tests/integration/exec.go | 90 + tests/integration/exec_test.go | 66 + tests/integration/integration_test.go | 593 +----- tests/integration/syscaller/cmd/syscall.go | 61 + .../integration/syscaller/cmd/syscall_test.go | 61 + tests/integration/syscaller/cmd/syscaller.go | 47 + tests/integration/tester.sh | 44 - tests/integration/tracee.go | 78 +- 14 files changed, 2149 insertions(+), 641 deletions(-) create mode 100644 tests/integration/cpu/cpu.go create mode 100644 tests/integration/event_filters_test.go create mode 100644 tests/integration/exec.go create mode 100644 tests/integration/exec_test.go create mode 100644 tests/integration/syscaller/cmd/syscall.go create mode 100644 tests/integration/syscaller/cmd/syscall_test.go create mode 100644 tests/integration/syscaller/cmd/syscaller.go delete mode 100755 tests/integration/tester.sh diff --git a/.gitignore b/.gitignore index 322ac9b09275..8175f3b2cf42 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ compile_commands.json # binaries and build files dist +tests/integration/syscaller/cmd/syscaller # release files release_notes.txt diff --git a/Makefile b/Makefile index 3231ffe27d23..95f9b405b7cb 100644 --- a/Makefile +++ b/Makefile @@ -36,6 +36,7 @@ CMD_STATICCHECK ?= staticcheck CMD_STRIP ?= llvm-strip CMD_TOUCH ?= touch CMD_TR ?= tr +CMD_SYSCALLER ?= ./tests/integration/syscaller/cmd/syscaller .check_%: # @@ -727,9 +728,22 @@ test-types: \ -coverprofile=coverage.txt \ ./... +# +# syscaller (required for integration tests) +# + +.PHONY: $(CMD_SYSCALLER) +$(CMD_SYSCALLER): \ + $(OUTPUT_DIR)/libbpf/libbpf.a \ + | .check_$(CMD_GO) +# + $(GO_ENV_EBPF) \ + $(CMD_GO) build -o $(CMD_SYSCALLER) $(dir $(CMD_SYSCALLER)) + .PHONY: test-integration test-integration: \ .checkver_$(CMD_GO) \ + $(CMD_SYSCALLER) \ tracee-ebpf # $(GO_ENV_EBPF) \ @@ -739,6 +753,7 @@ test-integration: \ -extldflags \"$(CGO_EXT_LDFLAGS_EBPF)\" \ -X main.version=\"$(VERSION)\" \ " \ + -race \ -v \ -p 1 \ -count=1 \ @@ -829,3 +844,4 @@ clean: $(CMD_RM) -f .*.md5 $(CMD_RM) -f .check* $(CMD_RM) -f .*-pkgs* + $(CMD_RM) -f $(CMD_SYSCALLER) diff --git a/go.mod b/go.mod index fa38b80fc749..40f420dd06d7 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/spf13/viper v1.15.0 github.com/stretchr/testify v1.8.2 github.com/urfave/cli/v2 v2.3.0 + go.uber.org/goleak v1.1.12 go.uber.org/zap v1.24.0 golang.org/x/sys v0.8.0 google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 diff --git a/go.sum b/go.sum index 85d986b4f360..1e6df9631dfe 100644 --- a/go.sum +++ b/go.sum @@ -462,6 +462,7 @@ go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqe go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= @@ -502,6 +503,7 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= diff --git a/tests/integration/cpu/cpu.go b/tests/integration/cpu/cpu.go new file mode 100644 index 000000000000..a20d278b2b58 --- /dev/null +++ b/tests/integration/cpu/cpu.go @@ -0,0 +1,18 @@ +package cpu + +import "golang.org/x/sys/unix" + +const CPUForTests = 0 // CPU to pin test processes to + +// SetCPUs pins the current process to a specific CPU +func SetCPUs(id ...int) { + if len(id) == 0 { + id = append(id, CPUForTests) + } + + cpuMask := unix.CPUSet{} + for _, i := range id { + cpuMask.Set(i) + } + _ = unix.SchedSetaffinity(0, &cpuMask) +} diff --git a/tests/integration/event_filters_test.go b/tests/integration/event_filters_test.go new file mode 100644 index 000000000000..3d96741298e9 --- /dev/null +++ b/tests/integration/event_filters_test.go @@ -0,0 +1,1712 @@ +package integration + +import ( + "context" + "fmt" + "sort" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/goleak" + + "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/policy" + "github.com/aquasecurity/tracee/pkg/utils" + "github.com/aquasecurity/tracee/signatures/helpers" + "github.com/aquasecurity/tracee/tests/integration/cpu" + "github.com/aquasecurity/tracee/types/trace" +) + +// Test_EventFilters tests a variety of trace event filters +// with different combinations of policies +func Test_EventFilters(t *testing.T) { + // Make sure we don't leak any goroutines since we run Tracee many times in this test. + // If a test case fails, ignore the leak since it's probably caused by the aborted test. + defer goleak.VerifyNone(t) + + // test table + tt := []testCase{ + // events matched in single policies - detached workloads + { + name: "container: event: trace only events from new containers", + policies: []*policy.Policy{ + newPolicy( + "container_event", + 1, + []string{ + "container=new", + "event!=container_create,container_remove", + }, + ), + }, + cmdEvents: []cmdEvents{ + newCmdEvents( + "docker run -d --rm hello-world", + 10*time.Second, // give some time for the container to start (possibly downloading the image) + []trace.Event{ + expectEvent(anyHost, "hello", anyProcessorID, 1, 0, events.SchedProcessExec, orPolNames("container_event"), orPolIDs(1)), + }, + []string{}, // no sets + ), + }, + useSyscaller: false, + test: ExpectAllInOrder, + }, + { + name: "comm: event: trace events set in a single policy from ping command", + policies: []*policy.Policy{ + newPolicy( + "comm_event", + 1, + []string{ + "comm=ping", + "event=sched_process_exec,sched_process_exit", + }, + ), + }, + cmdEvents: []cmdEvents{ + newCmdEvents( + "ping -c1 0.0.0.0", + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "ping", cpu.CPUForTests, anyPID, 0, events.SchedProcessExec, orPolNames("comm_event"), orPolIDs(1)), + expectEvent(anyHost, "ping", cpu.CPUForTests, anyPID, 0, events.SchedProcessExit, orPolNames("comm_event"), orPolIDs(1)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllInOrder, + }, + { + name: "comm: event: trace events set in a single policy from ping command", + policies: []*policy.Policy{ + newPolicy( + "comm_event", + 5, + []string{ + "comm=ping", + "event=net_packet_icmp", + }, + ), + }, + cmdEvents: []cmdEvents{ + newCmdEvents( + "ping -c1 0.0.0.0", + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "ping", cpu.CPUForTests, anyPID, 0, events.NetPacketICMP, orPolNames("comm_event"), orPolIDs(5)), + expectEvent(anyHost, "ping", cpu.CPUForTests, anyPID, 0, events.NetPacketICMP, orPolNames("comm_event"), orPolIDs(5)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllInOrder, + }, + { + name: "event: args: trace event set in a specific policy with args finishing with 'ls'", + policies: []*policy.Policy{ + newPolicy( + "event_args", + 42, + []string{ + "event=execve", + "execve.args.pathname=*ls", + }, + ), + }, + cmdEvents: []cmdEvents{ + newCmdEvents( + "ls", + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "integration.tes", // note that comm name is from the go test binary that runs the command + cpu.CPUForTests, anyPID, 0, events.Execve, orPolNames("event_args"), orPolIDs(42), expectArg("pathname", "*ls")), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllInOrder, + }, + { + name: "event: args: trace event set in a specific policy with args starting with * wildcard", + policies: []*policy.Policy{ + newPolicy( + "comm_event_args", + 42, + []string{ + "event=execve", + "execve.args.pathname=*/almost/improbable/path", // no event expected + }, + ), + }, + cmdEvents: []cmdEvents{ + // no event expected + newCmdEvents("ls", 1*time.Second, []trace.Event{}, []string{}), + newCmdEvents("uname", 1*time.Second, []trace.Event{}, []string{}), + newCmdEvents("who", 1*time.Second, []trace.Event{}, []string{}), + }, + useSyscaller: false, + test: ExpectAllInOrder, + }, + { + name: "comm: event: args: trace event set in a specific policy with args from ls command", + policies: []*policy.Policy{ + newPolicy( + "comm_event_args", + 42, + []string{ + "comm=ls", + "event=security_file_open", + "security_file_open.args.pathname=*integration", + }, + ), + }, + cmdEvents: []cmdEvents{ + newCmdEvents( + "ls", + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "ls", cpu.CPUForTests, anyPID, 0, events.SecurityFileOpen, orPolNames("comm_event_args"), orPolIDs(42), expectArg("pathname", "*integration")), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllInOrder, + }, + { + name: "comm: event: trace events set in two specific policies from ls and uname commands", + policies: []*policy.Policy{ + newPolicy( + "comm_event_4", + 4, + []string{ + "comm=ls", + "event=sched_process_exit", + }, + ), + newPolicy( + "comm_event_2", + 2, + []string{ + "comm=uname", + "event=sched_process_exit", + }, + ), + }, + cmdEvents: []cmdEvents{ + newCmdEvents("ls", + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "ls", cpu.CPUForTests, anyPID, 0, events.SchedProcessExit, orPolNames("comm_event_4"), orPolIDs(4)), + }, + []string{}, + ), + newCmdEvents("uname", + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "uname", cpu.CPUForTests, anyPID, 0, events.SchedProcessExit, orPolNames("comm_event_2"), orPolIDs(2)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllInOrder, + }, + { + name: "bin: event: trace events in separate policies from who and uname binary", + policies: []*policy.Policy{ + newPolicy( + "bin_event_1", + 1, + []string{ + "bin=/usr/bin/who", + "event=sched_process_exec", + }, + ), + newPolicy( + "bin_event_2", + 2, + []string{ + "bin=/usr/bin/uname", + "event=sched_process_exec", + }), + }, + cmdEvents: []cmdEvents{ + newCmdEvents("who", + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "who", cpu.CPUForTests, anyPID, 0, events.SchedProcessExec, orPolNames("bin_event_1"), orPolIDs(1)), + }, + []string{}, + ), + newCmdEvents("uname", + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "uname", cpu.CPUForTests, anyPID, 0, events.SchedProcessExec, orPolNames("bin_event_2"), orPolIDs(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", + policies: []*policy.Policy{ + newPolicy( + "pid_0_event_args", + 1, + []string{ + "pid=0", + "event=sched_switch", + "sched_switch.args.next_comm=systemd,init", + }, + ), + }, + cmdEvents: []cmdEvents{ + newCmdEvents( + "kill -SIGHUP 1", // reloads the complete daemon configuration + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, anyComm, anyProcessorID, 0, 0, events.SchedSwitch, orPolNames("pid_0_event_args"), orPolIDs(1), expectArg("next_comm", "systemd")), + expectEvent(anyHost, anyComm, anyProcessorID, 0, 0, events.SchedSwitch, orPolNames("pid_0_event_args"), orPolIDs(1), expectArg("next_comm", "init")), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAnyOfEach, + }, + { + name: "pid: trace events from pid 1", + policies: []*policy.Policy{ + newPolicy( + "pid_1", + 1, + []string{ + "pid=1", + }, + ), + }, + cmdEvents: []cmdEvents{ + newCmdEvents( + "kill -SIGHUP 1", // reloads the complete daemon configuration + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "init", anyProcessorID, 1, 0, anyEventID, orPolNames("pid_1"), orPolIDs(1)), + expectEvent(anyHost, "systemd", anyProcessorID, 1, 0, anyEventID, orPolNames("pid_1"), orPolIDs(1)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAnyOfEach, + }, + { + name: "uid: comm: trace uid 0 from ls command", + policies: []*policy.Policy{ + newPolicy( + "uid_0_comm", + 1, + []string{ + "uid=0", + "comm=ls", + }, + ), + }, + cmdEvents: []cmdEvents{ + newCmdEvents( + "ls", + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "ls", cpu.CPUForTests, anyPID, 0, anyEventID, orPolNames("uid_0_comm"), orPolIDs(1)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllEqualTo, + }, + { + name: "uid: comm: trace only uid>0 from ls command (should be empty)", + policies: []*policy.Policy{ + newPolicy( // no events expected + "uid_0_comm", + 1, + []string{ + "uid>0", + "comm=ls", + }, + ), + }, + cmdEvents: []cmdEvents{ + newCmdEvents( + "ls", + 1*time.Second, + []trace.Event{}, // no events expected + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllInOrder, + }, + { + name: "set: comm: trace filesystem events from ls command", + policies: []*policy.Policy{ + newPolicy( + "set_comm", + 1, + []string{ + "set=fs", + "comm=ls", + }, + ), + }, + cmdEvents: []cmdEvents{ + newCmdEvents( + "ls", + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "ls", cpu.CPUForTests, anyPID, 0, anyEventID, orPolNames("set_comm"), orPolIDs(1)), + }, + []string{"fs"}, + ), + }, + useSyscaller: false, + test: ExpectAllEqualTo, + }, + { + name: "bin: event: trace only setns events from \"/usr/bin/dockerd\" binary", + policies: []*policy.Policy{ + newPolicy( + "bin_event", + 1, + []string{ + "bin=/usr/bin/dockerd", + "event=setns", + }, + ), + }, + cmdEvents: []cmdEvents{ + newCmdEvents( + "docker run -d --rm hello-world", + 10*time.Second, // give some time for the container to start (possibly downloading the image) + []trace.Event{ + // using anyComm as some versions of dockerd may result in e.g. "dockerd" or "exe" + expectEvent(anyHost, anyComm, anyProcessorID, anyPID, 0, events.Setns, orPolNames("bin_event"), orPolIDs(1)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllInOrder, + }, + { + name: "trace new pids (should be empty)", + policies: []*policy.Policy{ + newPolicy( // no events expected + "pid_new", + 1, + []string{ + "pid=new", + "pid=1", + }, + ), + }, + cmdEvents: []cmdEvents{ + newCmdEvents( + "kill -SIGHUP 1", // reloads the complete daemon configuration + 1*time.Second, + []trace.Event{}, // no events expected + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllInOrder, + }, + { + name: "comm: trace events set in a specific policy from ls command", + policies: []*policy.Policy{ + newPolicy( + "comm_64", + 64, + []string{ + "comm=ls", + }, + ), + }, + cmdEvents: []cmdEvents{ + newCmdEvents( + "ls", + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "ls", cpu.CPUForTests, anyPID, 0, anyEventID, orPolNames("comm_64"), orPolIDs(64)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllEqualTo, + }, + { + name: "comm: trace events set in a specific policy from ls command", + policies: []*policy.Policy{ + newPolicy( + "comm_64", + 64, + []string{ + "comm=ls", + }, + ), + newPolicy( // no events expected + "comm_42", + 42, + []string{ + "comm=who", + }, + ), + }, + cmdEvents: []cmdEvents{ + newCmdEvents( + "ls", + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "ls", cpu.CPUForTests, anyPID, 0, anyEventID, orPolNames("comm_64"), orPolIDs(64)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllEqualTo, + }, + { + name: "comm: trace events set in a specific policy from ls and who commands", + policies: []*policy.Policy{ + newPolicy( + "comm_64", + 64, + []string{ + "comm=ls", + }, + ), + newPolicy( + "comm_42", + 42, + []string{ + "comm=who", + }, + ), + }, + cmdEvents: []cmdEvents{ + newCmdEvents( + "ls", + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "ls", cpu.CPUForTests, anyPID, 0, anyEventID, orPolNames("comm_64"), orPolIDs(64)), + }, + []string{}, + ), + newCmdEvents( + "who", + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "who", cpu.CPUForTests, anyPID, 0, anyEventID, orPolNames("comm_42"), orPolIDs(42)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllEqualTo, + }, + { + name: "event: args: context: only security_file_open from \"execve\" syscall", + policies: []*policy.Policy{ + newPolicy( + "event_args_context", + 42, + []string{ + "event=security_file_open", + "security_file_open.context.syscall=execve", + "security_file_open.args.pathname=*ls", + }, + ), + }, + cmdEvents: []cmdEvents{ + newCmdEvents( + "bash -c ls", + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "bash", // note that comm name is from the runner + cpu.CPUForTests, anyPID, 0, events.SecurityFileOpen, orPolNames("event_args_context"), orPolIDs(42), expectArg("pathname", "*ls")), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllEqualTo, + }, + { + name: "comm: event: do a file write", + policies: []*policy.Policy{ + newPolicy( + "comm_event", + 42, + []string{ + "comm=tee", + "event=magic_write", + }, + ), + }, + cmdEvents: []cmdEvents{ + newCmdEvents( + "bash -c '/usr/bin/tee /tmp/magic_write_test < <(echo 42)'", + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "tee", cpu.CPUForTests, anyPID, 0, events.MagicWrite, orPolNames("comm_event"), orPolIDs(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: args: sign: trace sys events + signature events in separte policies", + // policies: []*policy.Policy{ + // newPolicy( + // "comm_event", + // 3, + // []string{ + // "comm=ping", + // "event=net_packet_icmp", + // }, + // ), + // newPolicy( + // "event_args", + // 5, + // []string{ + // "event=ptrace", + // "ptrace.args.pid=0", + // }, + // ), + // newPolicy( + // "sign", + // 9, + // []string{ + // "event=anti_debugging", + // }, + // ), + // }, + // cmdEvents: []cmdEvents{ + // newCmdEvents( + // "ping -c1 0.0.0.0", + // 1*time.Second, + // []trace.Event{ + // expectEvent(anyHost, "ping", cpu.CPUForTests, anyPID, 0, events.NetPacketICMP, "comm_event", orPolicies(3)), + // }, + // []string{}, + // ), + // newCmdEvents( + // "strace ls", + // 1*time.Second, + // []trace.Event{ + // expectEvent(anyHost, "strace", cpu.CPUForTests, anyPID, 0, events.Ptrace, "event_args", orPolicies(5)), + // expectEvent(anyHost, "strace", cpu.CPUForTests, anyPID, 0, events.anti_debugging, "sign", orPolicies(9)), + // }, + // []string{}, + // ), + // }, + // useSyscaller: false, + // test: ExpectAtLeastOneOfEach, + // }, + + // events matched in multiple policies - intertwined workloads + { + name: "comm: event: trace events from ping command in multiple policies", + policies: []*policy.Policy{ + newPolicy( + "comm_event_3", + 3, + []string{ + "comm=ping", + "event=net_packet_icmp", + }, + ), + newPolicy( + "comm_event_5", + 5, + []string{ + "comm=ping", + "event=net_packet_icmp", + }, + ), + }, + cmdEvents: []cmdEvents{ + newCmdEvents( + "ping -c1 0.0.0.0", + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "ping", cpu.CPUForTests, anyPID, 0, events.NetPacketICMP, orPolNames("comm_event_3", "comm_event_5"), orPolIDs(3, 5)), + expectEvent(anyHost, "ping", cpu.CPUForTests, anyPID, 0, events.NetPacketICMP, orPolNames("comm_event_3", "comm_event_5"), orPolIDs(3, 5)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllInOrder, + }, + { + name: "comm: event: trace events from ping command in multiple policies", + policies: []*policy.Policy{ + newPolicy( + "comm_event_3", + 3, + []string{ + "comm=ping", + "event=net_packet_icmp", + }, + ), + newPolicy( + "comm_event_5", + 5, + []string{ + "comm=ping", + "event=net_packet_icmp,setuid", + }, + ), + }, + cmdEvents: []cmdEvents{ + newCmdEvents( + "ping -c1 0.0.0.0", + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "ping", cpu.CPUForTests, anyPID, 0, events.Setuid, orPolNames("comm_event_5"), orPolIDs(5)), + + expectEvent(anyHost, "ping", cpu.CPUForTests, anyPID, 0, events.NetPacketICMP, orPolNames("comm_event_3", "comm_event_5"), orPolIDs(3, 5)), + expectEvent(anyHost, "ping", cpu.CPUForTests, anyPID, 0, events.NetPacketICMP, orPolNames("comm_event_3", "comm_event_5"), orPolIDs(3, 5)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllInOrder, + }, + { + name: "comm: event: trace events from ping command in multiple policies", + policies: []*policy.Policy{ + newPolicy( + "comm_event_3", + 3, + []string{ + "comm=ping", + "event=net_packet_icmp", + }, + ), + newPolicy( + "comm_event_5", + 5, + []string{ + "comm=ping", + "event=net_packet_icmp", + }, + ), + newPolicy( + "comm_event_7", + 7, + []string{ + "comm=ping", + "event=sched_process_exec", + }, + ), + newPolicy( + "comm_event_9", + 9, + []string{ + "comm=ping", + "event=sched_process_exec,security_socket_connect", + }, + ), + }, + cmdEvents: []cmdEvents{ + newCmdEvents( + "ping -c1 0.0.0.0", + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "ping", cpu.CPUForTests, anyPID, 0, events.SchedProcessExec, orPolNames("comm_event_7", "comm_event_9"), orPolIDs(7, 9)), + + expectEvent(anyHost, "ping", cpu.CPUForTests, anyPID, 0, events.SecuritySocketConnect, orPolNames("comm_event_9"), orPolIDs(9)), + + expectEvent(anyHost, "ping", cpu.CPUForTests, anyPID, 0, events.NetPacketICMP, orPolNames("comm_event_3", "comm_event_5"), orPolIDs(3, 5)), + expectEvent(anyHost, "ping", cpu.CPUForTests, anyPID, 0, events.NetPacketICMP, orPolNames("comm_event_3", "comm_event_5"), orPolIDs(3, 5)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllInOrder, + }, + { + name: "comm: trace only events from from ls and who commands in multiple policies", + policies: []*policy.Policy{ + newPolicy( + "comm_64", + 64, + []string{ + "comm=ls", + }, + ), + newPolicy( + "comm_42", + 42, + []string{ + "comm=who,ls", + }, + ), + }, + cmdEvents: []cmdEvents{ + newCmdEvents( + "ls", + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "ls", cpu.CPUForTests, anyPID, 0, anyEventID, orPolNames("comm_64", "comm_42"), orPolIDs(64, 42)), + }, + []string{}, + ), + newCmdEvents( + "who", + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "who", cpu.CPUForTests, anyPID, 0, anyEventID, orPolNames("comm_42"), orPolIDs(42)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAllEqualTo, + }, + { + name: "comm: trace at least one event in multiple policies from ls and who commands", + policies: []*policy.Policy{ + newPolicy( + "comm_64", + 64, + []string{ + "comm=ls", + }, + ), + newPolicy( + "comm_42", + 42, + []string{ + "comm=who,ls", + }, + ), + }, + cmdEvents: []cmdEvents{ + newCmdEvents( + "ls", + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "ls", cpu.CPUForTests, anyPID, 0, anyEventID, orPolNames("comm_64", "comm_42"), orPolIDs(64, 42)), + }, + []string{}, + ), + newCmdEvents( + "who", + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "who", cpu.CPUForTests, anyPID, 0, anyEventID, orPolNames("comm_42"), orPolIDs(42)), + }, + []string{}, + ), + }, + useSyscaller: false, + test: ExpectAtLeastOneOfEach, + }, + + // This uses the syscaller tool which emits the desired events from a desired comm, + // what is useful for testing events that are not easily triggered by a program. + // In the following example, setting useSyscaller to true we use it to: + // - impersonate a comm of "fakeprog1", based on runCmd arg passed in newCmdEvents + // - emit read and write events, as defined in expected events + { + name: "comm: event: trace events read and write set in a single policy from fakeprog1 command", + policies: []*policy.Policy{ + newPolicy( + "comm_event", + 1, + []string{ + "comm=fakeprog1", + "event=read,write", + }, + ), + }, + cmdEvents: []cmdEvents{ + newCmdEvents( + "fakeprog1", + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "fakeprog1", cpu.CPUForTests, anyPID, 0, events.Read, orPolNames("comm_event"), orPolIDs(1)), + expectEvent(anyHost, "fakeprog1", cpu.CPUForTests, anyPID, 0, events.Write, orPolNames("comm_event"), orPolIDs(1)), + }, + []string{}, + ), + }, + useSyscaller: true, + test: ExpectAllInOrder, + }, + { + name: "event: trace execve event set in a specific policy from fakeprog1 command", + policies: []*policy.Policy{ + newPolicy( + "event_pol_42", + 42, + []string{ + "event=execve", + }, + ), + }, + cmdEvents: []cmdEvents{ + newCmdEvents( + "fakeprog1", + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "fakeprog1", cpu.CPUForTests, anyPID, 0, events.Execve, orPolNames("event_pol_42"), orPolIDs(42)), + }, + []string{}, + ), + }, + useSyscaller: true, + test: ExpectAtLeastOneOfEach, + }, + { + name: "comm: event: args: trace event set in a specific policy with args from fakeprog1 and fakeprog2 commands", + policies: []*policy.Policy{ + newPolicy( + "comm_event_args_64", + 64, + []string{ + "comm=fakeprog1", + "event=openat", + "openat.args.dirfd=0", + "openat.args.flags=0", + "openat.args.mode=0", + }, + ), + newPolicy( + "comm_event_args_42", + 42, + []string{ + "comm=fakeprog2", + "event=open", + "open.args.flags=0", + "open.args.mode=0", + }, + ), + }, + cmdEvents: []cmdEvents{ + newCmdEvents( + "fakeprog1", + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "fakeprog1", cpu.CPUForTests, anyPID, 0, events.Openat, orPolNames("comm_event_args_64"), orPolIDs(64), + expectArg("dirfd", int32(0)), + expectArg("flags", int32(0)), + expectArg("mode", uint32(0)), + ), + }, + []string{}, + ), + newCmdEvents( + "fakeprog2", + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "fakeprog2", cpu.CPUForTests, anyPID, 0, events.Open, orPolNames("comm_event_args_42"), orPolIDs(42), + expectArg("flags", int32(0)), + expectArg("mode", uint32(0)), + ), + }, + []string{}, + ), + }, + useSyscaller: true, + test: ExpectAllInOrder, + }, + { + name: "comm: event: retval: trace event set in a specific policy with retval from fakeprog1 and fakeprog2 commands", + policies: []*policy.Policy{ + newPolicy( + "comm_event_retval_64", + 64, + []string{ + "comm=fakeprog1", + "event=openat", + "openat.retval<0", + }, + ), + newPolicy( // no events expected + "comm_event_retval_42", + 42, + []string{ + "comm=fakeprog2", + "event=open", + "open.retval>=0", + }, + ), + }, + cmdEvents: []cmdEvents{ + newCmdEvents( + "fakeprog1", + 1*time.Second, + []trace.Event{ + expectEvent(anyHost, "fakeprog1", cpu.CPUForTests, anyPID, 0, events.Openat, orPolNames("comm_event_retval_64"), orPolIDs(64), + expectArg("dirfd", int32(0)), + expectArg("flags", int32(0)), + expectArg("mode", uint32(0)), + ), + }, + []string{}, + ), + newCmdEvents( + "fakeprog2", + 1*time.Second, + []trace.Event{}, // no events expected + []string{}, + ), + }, + useSyscaller: true, + test: ExpectAllInOrder, + }, + } + + // run tests cases + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + // prepare tracee config + config := tracee.Config{ + Policies: newPolicies(tc.policies...), + ChanEvents: make(chan trace.Event, 1000), + Capabilities: &tracee.CapabilitiesConfig{ + BypassCaps: true, + }, + } + + ctx, cancel := context.WithCancel(context.Background()) + + // start a goroutine to read events from the channel into the buffer + buf := &eventBuffer{} + go func(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + case evt := <-config.ChanEvents: + buf.mu.Lock() + buf.events = append(buf.events, evt) + buf.mu.Unlock() + } + } + }(ctx) + + // start tracee + trc := startTracee(ctx, t, config, nil, nil) + waitforTraceeStart(t, trc) + + // run a test case and validate the results against the expected events + tc.test(t, tc.cmdEvents, buf, tc.useSyscaller) + + // if we got here, the test passed, so we can stop tracee + cancel() + waitforTraceeStop(t, trc) + }) + } +} + +const ( + anyProcessorID = -1 + anyHost = "" + anyComm = "" + anyEventID = -1 + anyPID = -1 + anyUID = -1 + anyPolicy = 0 + anyPolicyName = "" +) + +type testCase struct { + name string + policies []*policy.Policy + cmdEvents []cmdEvents + useSyscaller bool + test func(t *testing.T, cmdEvents []cmdEvents, actual *eventBuffer, useSyscaller bool) +} + +type cmdEvents struct { + runCmd string + timeout time.Duration + evts []trace.Event + sets []string +} + +// newCmdEvents is a helper function to create a cmdEvents +func newCmdEvents(runCmd string, timeout time.Duration, evts []trace.Event, sets []string) cmdEvents { + return cmdEvents{ + runCmd: runCmd, + timeout: timeout, + evts: evts, + sets: sets, + } +} + +// newPolicy creates a new policy with the given name, ID and filter flags +func newPolicy(name string, id int, filterFlags []string) *policy.Policy { + filterMap, err := flags.PrepareFilterMapFromFlags(filterFlags) + if err != nil { + panic(err) + } + + pols, err := flags.CreatePolicies(filterMap, true) + if err != nil { + panic(err) + } + + // we need to set the pol name and ID manually because we are not using the pol engine + // and we don't want to duplicate the logic of the pol engine here + pol, err := pols.Lookup(0) + if err != nil { + panic(err) + } + + pol.Name = name + pol.ID = id - 1 + pols.Set(pol) // set policy ID using Set() method which checks ID range + + return pol +} + +// newPolicies creates a new policies object with the given policies +func newPolicies(pols ...*policy.Policy) *policy.Policies { + policies := policy.NewPolicies() + + for _, pol := range pols { + policies.Set(pol) + } + + return policies +} + +// orPolIDs is a helper function to create a bit mask of the given policies IDs +func orPolIDs(policies ...uint) uint64 { + var res uint64 + + for _, pol := range policies { + utils.SetBit(&res, pol-1) + } + + return res +} + +// orPolNames is a helper function to create a slice of the given policies names +func orPolNames(policies ...string) []string { + return policies +} + +// 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, + } +} + +// revive:disable:argument-limit +// expectEvent is a helper function to create a trace.Event +func expectEvent( + host, comm string, + processorID, pid, uid int, + eventID events.ID, + matchPolName []string, + matchPols uint64, + args ...trace.Argument, +) trace.Event { + return trace.Event{ + ProcessorID: processorID, + ProcessID: pid, + UserID: uid, + ProcessName: comm, + HostName: host, + EventID: int(eventID), + MatchedPolicies: matchPols, + MatchedPoliciesNames: matchPolName, + 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, cmd cmdEvents, actual *eventBuffer, useSyscaller, failOnTimeout bool) proc { + var ( + pid int + err error + ) + + if useSyscaller { + formatCmdEvents(&cmd) + } + pid, err = execCmd(cmd.runCmd, cmd.timeout) + require.NoError(t, err) + + waitForTraceeOutputEvents(t, actual, time.Now(), len(cmd.evts), failOnTimeout) + + return proc{ + pid: pid, + expectedEvts: len(cmd.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 + ) + + if useSyscaller { + formatCmdEvents(&cmd) + } + pid, err = execCmd(cmd.runCmd, cmd.timeout) + 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 +} + +// formatCmdEvents formats given commands to be executed by syscaller helper tool +func formatCmdEvents(cmd *cmdEvents) { + cmd.runCmd = fmt.Sprintf("./syscaller/cmd/syscaller %s", cmd.runCmd) + for _, evt := range cmd.evts { + cmd.runCmd = fmt.Sprintf("%s %d", cmd.runCmd, evt.EventID) + } +} + +// 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 +} + +// copyActualEvents returns a copy of the actual events +// This is to avoid holding the lock while comparing the events (in nested loops) +func copyActualEvents(actual *eventBuffer) []trace.Event { + var evts []trace.Event + + actual.mu.Lock() + evts = append(evts, actual.events...) + actual.mu.Unlock() + + return evts +} + +// assert that the given string slices are equal, ignoring order +func assertUnorderedStringSlicesEqual(t *testing.T, expNames []string, actNames []string) { + assert.Equal(t, len(expNames), len(actNames)) + sortedExpNames := make([]string, len(expNames)) + copy(sortedExpNames, expNames) + sort.Strings(sortedExpNames) + + sortedActNames := make([]string, len(actNames)) + copy(sortedActNames, actNames) + sort.Strings(sortedActNames) + + for i := range sortedExpNames { + assert.Equal(t, sortedExpNames[i], sortedActNames[i]) + } +} + +// 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) + } + + actEvtsCopy := copyActualEvents(actual) + + // second stage: validate events + 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 + checkPolicy := expEvt.MatchedPolicies != anyPolicy + checkPolicyName := len(expEvt.MatchedPoliciesNames) > 0 && expEvt.MatchedPoliciesNames[0] != anyPolicyName + + if len(exp.evts) > 0 && proc.expectedEvts == 0 { + t.Fatalf("expected events for command %s, but got none", exp.runCmd) + } + + for _, actEvt := range actEvtsCopy { + 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 checkPolicy && actEvt.MatchedPolicies != expEvt.MatchedPolicies { + continue + } + if checkPolicyName { + polNameFound := false + for _, policyName := range expEvt.MatchedPoliciesNames { + for _, actPolicyName := range actEvt.MatchedPoliciesNames { + if policyName == actPolicyName { + polNameFound = true + break + } + } + if polNameFound { + break + } + } + + if !polNameFound { + 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) + } + } +} + +// ExpectAnyOfEach validates that at any event from each command was captured +func ExpectAnyOfEach(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) + } + + actEvtsCopy := copyActualEvents(actual) + + // second stage: validate events + found := false + 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 + checkPolicy := expEvt.MatchedPolicies != anyPolicy + checkPolicyName := len(expEvt.MatchedPoliciesNames) > 0 && expEvt.MatchedPoliciesNames[0] != anyPolicyName + + if len(exp.evts) > 0 && proc.expectedEvts == 0 { + t.Fatalf("expected events for command %s, but got none", exp.runCmd) + } + + for _, actEvt := range actEvtsCopy { + 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 checkPolicy && actEvt.MatchedPolicies != expEvt.MatchedPolicies { + continue + } + if checkPolicyName { + polNameFound := false + for _, policyName := range expEvt.MatchedPoliciesNames { + for _, actPolicyName := range actEvt.MatchedPoliciesNames { + if policyName == actPolicyName { + polNameFound = true + break + } + } + if polNameFound { + break + } + } + + if !polNameFound { + 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 + } + + if found { + break + } + } + + // evaluate found + require.True(t, found, "None of the expected events\n%+v\nare in the actual output\n%+v\n", exp.evts, actEvtsCopy) + } +} + +// 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) + actEvtsCopy := copyActualEvents(actual) + + 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 + 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 + checkPolicy := expEvt.MatchedPolicies != anyPolicy + checkPolicyName := len(expEvt.MatchedPoliciesNames) > 0 && expEvt.MatchedPoliciesNames[0] != anyPolicyName + + for _, actEvt := range actEvtsCopy { + 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 checkPolicy { + assert.Equal(t, expEvt.MatchedPolicies, actEvt.MatchedPolicies, "matched policies") + } + if checkPolicyName { + assertUnorderedStringSlicesEqual(t, expEvt.MatchedPoliciesNames, actEvt.MatchedPoliciesNames) + } + + // 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") + } + } + } + } + } +} + +// 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)) + } + actEvtsCopy := copyActualEvents(actual) + + // second stage: check events + 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 := actEvtsCopy[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 + checkPolicy := expEvt.MatchedPolicies != anyPolicy + checkPolicyName := len(expEvt.MatchedPoliciesNames) > 0 && expEvt.MatchedPoliciesNames[0] != anyPolicyName + + 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 checkPolicy { + assert.Equal(t, expEvt.MatchedPolicies, actEvt.MatchedPolicies, "matched policies") + } + if checkPolicyName { + assertUnorderedStringSlicesEqual(t, expEvt.MatchedPoliciesNames, actEvt.MatchedPoliciesNames) + } + + // 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") + } + } + } + } +} diff --git a/tests/integration/exec.go b/tests/integration/exec.go new file mode 100644 index 000000000000..93f2a6b3db97 --- /dev/null +++ b/tests/integration/exec.go @@ -0,0 +1,90 @@ +package integration + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strings" + "time" + + "github.com/aquasecurity/tracee/tests/integration/cpu" +) + +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[1:] + // remove single quotes from args, since they can confuse exec + for i, arg := range args { + args[i] = strings.Trim(arg, "'") + } + + return cmd, args, nil +} + +func execCmd(command string, timeout time.Duration) (int, error) { + cpu.SetCPUs() + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + command, args, err := parseCmd(command) + if err != nil { + fmt.Fprintf(os.Stderr, "parseCmd: %s", err) + return 0, err + } + + fmt.Fprintf(os.Stderr, "\texecuting: %s %v\n", command, args) + cmd := exec.Command(command, args...) + err = cmd.Start() + if err != nil { + return 0, fmt.Errorf("failed to start command: %s", err) + } + + pid := cmd.Process.Pid + + // wait for the command to finish or for the timeout to expire + done := make(chan error) + go func() { + done <- cmd.Wait() + }() + + select { + case <-time.After(timeout): + // timed out + err := cmd.Process.Kill() + if err != nil { + return pid, fmt.Errorf("command timed out, failed to kill process: %s", err) + } + return pid, fmt.Errorf("command timed out after %s", timeout) + + case err := <-done: + // command completed + if err != nil { + return pid, fmt.Errorf("command failed with error: %s", err) + } + return pid, nil + } +} diff --git a/tests/integration/exec_test.go b/tests/integration/exec_test.go new file mode 100644 index 000000000000..0f159500cfab --- /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{"hello"}, + expectedErrMsg: "", + }, + { + input: "/usr/bin/echo hello", + expectedCmd: "/usr/bin/echo", + expectedArgs: []string{"hello"}, + expectedErrMsg: "", + }, + { + input: "echo 'hello world'", + expectedCmd: "/usr/bin/echo", + expectedArgs: []string{"hello world"}, + expectedErrMsg: "", + }, + { + input: "bash -c 'echo hello world'", + expectedCmd: "/usr/bin/bash", + expectedArgs: []string{"-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 d147ff363223..c5aab2033e11 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -1,29 +1,15 @@ package integration import ( - "context" _ "embed" - "fmt" - "io" - "os" - "strconv" - "strings" - "syscall" "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/policy" - "github.com/aquasecurity/tracee/signatures/helpers" - "github.com/aquasecurity/tracee/types/trace" ) -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) @@ -39,580 +25,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 *eventOutput) { - _, err := forkAndExecFunction(doMagicWrite) - require.NoError(t, err) - - waitForTraceeOutput(t, gotOutput, time.Now(), true) - - // check tracee output - output := gotOutput.getEventsCopy() - for _, evt := range output { - assert.Equal(t, []byte(evt.EventName), []byte("magic_write")) - } -} - -// execute a ls command -func checkExeccommand(t *testing.T, gotOutput *eventOutput) { - _, err := forkAndExecFunction(doLs) - require.NoError(t, err) - - waitForTraceeOutput(t, gotOutput, time.Now(), true) - - // check tracee output - processNames := []string{} - output := gotOutput.getEventsCopy() - for _, evt := range output { - 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 *eventOutput) { - 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{} - output := gotOutput.getEventsCopy() - for _, evt := range output { - 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 *eventOutput) { - _, 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{} - output := gotOutput.getEventsCopy() - for _, evt := range output { - 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 *eventOutput) { - _, 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 *eventOutput) { - _, 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{} - output := gotOutput.getEventsCopy() - for _, evt := range output { - 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 *eventOutput) { - _, 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{} - output := gotOutput.getEventsCopy() - for _, evt := range output { - eventNames = append(eventNames, evt.EventName) - } - for _, en := range eventNames { - require.Contains(t, expectedSyscalls, en) - } -} - -func checkNewContainers(t *testing.T, gotOutput *eventOutput) { - // pull alpine image first otherwise pull might happen - // on docker run and cause output to be too delayed - _, err := forkAndExecFunction(doPullAlpine) - require.NoError(t, err) - - // start test - containerIdBytes, err := forkAndExecFunction(doDockerRun) - require.NoError(t, err) - containerId := strings.TrimSuffix(string(containerIdBytes), "\n") - require.NotEmpty(t, containerId) - containerIds := []string{} - output := gotOutput.getEventsCopy() - for _, evt := range output { - containerIds = append(containerIds, evt.Container.ID) - } - 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 *eventOutput) { - _, err := forkAndExecFunction(doFileOpen) - require.NoError(t, err) - - output := gotOutput.getEventsCopy() - for _, evt := range output { - assert.Equal(t, "execve", evt.Syscall) - } -} - -func checkDockerdBinaryFilter(t *testing.T, gotOutput *eventOutput) { - 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{} - output := gotOutput.getEventsCopy() - for _, evt := range output { - processIds = append(processIds, evt.ProcessID) - } - assert.Contains(t, processIds, int(dockerdPid)) -} - -func TestEventFilters(t *testing.T) { - testCases := []struct { - name string - filterArgs []string - eventFunc func(*testing.T, *eventOutput) - }{ - { - 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 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, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - - filterMap, err := flags.PrepareFilterMapFromFlags(tc.filterArgs) - require.NoError(t, err) - - policies, err := flags.CreatePolicies(filterMap, false) - - require.NoError(t, err) - - eventChan := make(chan trace.Event, 1000) - config := tracee.Config{ - ChanEvents: eventChan, - Capabilities: &tracee.CapabilitiesConfig{ - BypassCaps: true, - }, - } - config.Policies = policies - eventOutput := &eventOutput{} - - go func() { - for { - select { - case evt, ok := <-eventChan: - if !ok { - return - } - eventOutput.addEvent(evt) - - case <-ctx.Done(): - return - } - } - }() - - trc := startTracee(ctx, t, config, nil, nil) - - waitforTraceeStart(t, trc, time.Now()) - - tc.eventFunc(t, eventOutput) - - cancel() - }) - } -} - -func checkPolicySecurityFileOpenLs(t *testing.T, pols *policy.Policies, gotOutput *eventOutput) { - _, err := forkAndExecFunction(doLs) - require.NoError(t, err) - - waitForTraceeOutput(t, gotOutput, time.Now(), true) - - output := gotOutput.getEventsCopy() - pol1, err := pols.Lookup(0) - require.NoError(t, err) - for _, evt := range output { - // ls - policy 1 - assert.Equal(t, "ls", evt.ProcessName, "ProcessName") - assert.Equal(t, uint64(1<<0), evt.MatchedPolicies, "MatchedPolicies") - assert.Equal(t, []string{pol1.Name}, evt.MatchedPoliciesNames, "MatchedPoliciesNames") - arg, err := helpers.GetTraceeArgumentByName(evt, "pathname", helpers.GetArgOps{DefaultArgs: false}) - require.NoError(t, err) - assert.Contains(t, arg.Value, "integration") - } -} - -// checkExecveOnPolicies1And2 demands an ordered events submission -func checkExecveOnPolicies1And2(t *testing.T, pols *policy.Policies, gotOutput *eventOutput) { - _, err := forkAndExecFunction(doLsUname) - require.NoError(t, err) - - waitForTraceeOutput(t, gotOutput, time.Now(), true) - - // check output length - output := gotOutput.getEventsCopy() - require.Len(t, output, 2) - var evts [2]trace.Event - - // output should only have events with event name of execve - for i, evt := range output { - assert.Equal(t, "sched_process_exit", evt.EventName) - evts[i] = evt - } - pol1, err := pols.Lookup(0) - require.NoError(t, err) - pol2, err := pols.Lookup(1) - require.NoError(t, err) - - // ls - policy 1 - assert.Equal(t, evts[0].ProcessName, "ls") - assert.Equal(t, uint64(1<<0), evts[0].MatchedPolicies, "MatchedPolicies") - assert.Equal(t, []string{pol1.Name}, evts[0].MatchedPoliciesNames, "MatchedPoliciesNames") - - // uname - policy 2 - assert.Equal(t, evts[1].ProcessName, "uname") - assert.Equal(t, uint64(1<<1), evts[1].MatchedPolicies, "MatchedPolicies") - assert.Equal(t, []string{pol2.Name}, evts[1].MatchedPoliciesNames, "MatchedPoliciesNames") -} - -func checkUnameAndWhoOnPoliciesWithBinaryScope(t *testing.T, pols *policy.Policies, gotOutput *eventOutput) { - var err error - _, err = forkAndExecFunction(doUnameWho) - require.NoError(t, err) - - waitForTraceeOutput(t, gotOutput, time.Now(), true) - - output := gotOutput.getEventsCopy() - for _, evt := range output { - procName := evt.ProcessName - if procName != "uname" && procName != "who" { - t.Fail() - } - } -} - -func TestEventPolicies(t *testing.T) { - testCases := []struct { - name string - policies []flags.PolicyFile - eventFunc func(*testing.T, *policy.Policies, *eventOutput) - }{ - { - name: "global scope - single event - args", - policies: []flags.PolicyFile{ - { - Name: "global_scope_single_event", - Description: "global scope - single event", - Scope: []string{"global"}, - DefaultAction: "log", - Rules: []flags.Rule{ - { - Event: "security_file_open", - Filter: []string{"args.pathname=*integration"}, - }, - }, - }, - }, - eventFunc: checkPolicySecurityFileOpenLs, - }, - { - name: "global scope - single event - comm", - policies: []flags.PolicyFile{ - { - Name: "global_scope_single_event_1", - Description: "global scope - single event 1", - Scope: []string{"global"}, - DefaultAction: "log", - Rules: []flags.Rule{ - { - Event: "sched_process_exit", - Filter: []string{"comm=ls"}, - }, - }, - }, - { - Name: "global_scope_single_event_2", - Description: "global scope - single event 2", - Scope: []string{"global"}, - DefaultAction: "log", - Rules: []flags.Rule{ - { - Event: "sched_process_exit", - Filter: []string{"comm=uname"}, - }, - }, - }, - }, - eventFunc: checkExecveOnPolicies1And2, - }, - { - name: "binary scope - single event", - policies: []flags.PolicyFile{ - { - Name: "binary_scope_single_event_1", - Description: "binary scope - single event 1", - Scope: []string{"binary=/usr/bin/uname"}, - DefaultAction: "log", - Rules: []flags.Rule{ - { - Event: "sched_process_exit", - }, - }, - }, - { - Name: "binary_scope_single_event_2", - Description: "binary scope - single event 2", - Scope: []string{"binary=/usr/bin/who"}, - DefaultAction: "log", - Rules: []flags.Rule{ - { - Event: "sched_process_exit", - }, - }, - }, - }, - eventFunc: checkUnameAndWhoOnPoliciesWithBinaryScope, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - filterMap, err := flags.PrepareFilterMapFromPolicies(tc.policies) - require.NoError(t, err) - - policies, err := flags.CreatePolicies(filterMap, false) - require.NoError(t, err) - - ctx, cancel := context.WithCancel(context.Background()) - - eventChan := make(chan trace.Event, 1000) - config := tracee.Config{ - ChanEvents: eventChan, - Capabilities: &tracee.CapabilitiesConfig{ - BypassCaps: true, - }, - } - config.Policies = policies - eventOutput := &eventOutput{} - - go func() { - for { - select { - case evt, ok := <-eventChan: - if !ok { - return - } - eventOutput.addEvent(evt) - - case <-ctx.Done(): - return - } - } - }() - - trc := startTracee(ctx, t, config, nil, nil) - - waitforTraceeStart(t, trc, time.Now()) - - tc.eventFunc(t, policies, eventOutput) - - cancel() - }) - } -} - -type testFunc string - -const ( - doMagicWrite testFunc = "do_magic_write" - doLs testFunc = "do_ls" - doLsUname testFunc = "do_ls_uname" - doUnameWho testFunc = "do_uname_who" - doDockerRun testFunc = "do_docker_run" - doPullAlpine testFunc = "do_docker_pull_alpine" - doFileOpen testFunc = "do_file_open" - getDockerdPid testFunc = "get_dockerd_pid" -) - -//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/cmd/syscall.go b/tests/integration/syscaller/cmd/syscall.go new file mode 100644 index 000000000000..29035df57dde --- /dev/null +++ b/tests/integration/syscaller/cmd/syscall.go @@ -0,0 +1,61 @@ +package main + +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 errno syscall.Errno + + if s, found := syscallMap[sysNum]; found { + _, _, errno = syscall.RawSyscall6(uintptr(sysNum), s.arg1, s.arg2, s.arg3, s.arg4, s.arg5, s.arg6) + } else { + _, _, errno = syscall.RawSyscall6(uintptr(sysNum), 0, 0, 0, 0, 0, 0) + } + if errno != 0 { + errs = append(errs, syscall.Errno(errno)) + } + } + + return errs +} diff --git a/tests/integration/syscaller/cmd/syscall_test.go b/tests/integration/syscaller/cmd/syscall_test.go new file mode 100644 index 000000000000..0804fe0c39d2 --- /dev/null +++ b/tests/integration/syscaller/cmd/syscall_test.go @@ -0,0 +1,61 @@ +package main + +import ( + "os" + "runtime" + "syscall" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/tracee/pkg/events" + "github.com/aquasecurity/tracee/tests/integration/cpu" +) + +// 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) { + cpu.SetCPUs() + 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) + } + + // 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/syscaller/cmd/syscaller.go b/tests/integration/syscaller/cmd/syscaller.go new file mode 100644 index 000000000000..8449983abe1b --- /dev/null +++ b/tests/integration/syscaller/cmd/syscaller.go @@ -0,0 +1,47 @@ +package main + +import ( + "fmt" + "os" + "runtime" + "strconv" + + "github.com/aquasecurity/tracee/pkg/events" + "github.com/aquasecurity/tracee/tests/integration/cpu" +) + +func main() { + cpu.SetCPUs() + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + if len(os.Args) < 3 { + fmt.Println("usage: syscaller caller_comm sycall_number[...]") + os.Exit(0) + } + + callerComm := os.Args[1] + syscallsToCall := make([]events.ID, 0) + for _, arg := range os.Args[2:] { + syscallNum, err := strconv.Atoi(arg) + if err != nil { + fmt.Fprintf(os.Stderr, "invalid syscall number: %s\n", arg) + os.Exit(1) + } + syscallsToCall = append(syscallsToCall, events.ID(syscallNum)) + } + + err := changeOwnComm(callerComm) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + + // do the magic + errs := callsys(syscallsToCall) + if len(errs) > 0 { + fmt.Fprintf(os.Stderr, "errors: %v\n", errs) + } + + os.Exit(0) +} diff --git a/tests/integration/tester.sh b/tests/integration/tester.sh deleted file mode 100755 index 760336293410..000000000000 --- a/tests/integration/tester.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/sh - -do_magic_write() { - tmpFileName=$1 - echo "AAAAA" > $tmpFileName -} - -do_ls() { - ls > /dev/null -} - -do_ls_uname() { - # run on the same core to ensure event order - taskset -c 0 ls; uname -} > /dev/null - -do_uname_who() { - # run on the same core to ensure event order - taskset -c 0 uname; who -} > /dev/null - -do_docker_pull_alpine() { - docker pull alpine -} > /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 b50a97c50732..5804c6663d8f 100644 --- a/tests/integration/tracee.go +++ b/tests/integration/tracee.go @@ -16,6 +16,28 @@ import ( "github.com/aquasecurity/tracee/types/trace" ) +// 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(ctx context.Context, t *testing.T, config tracee.Config, output *tracee.OutputConfig, capture *tracee.CaptureConfig) *tracee.Tracee { initialize.SetLibbpfgoCallbacks() @@ -77,6 +99,7 @@ func startTracee(ctx context.Context, t *testing.T, config tracee.Config, output return trc } +// prepareCapture prepares a capture config for tracee func prepareCapture() *tracee.CaptureConfig { // taken from tracee-rule github project, might have to adjust... // prepareCapture is called with nil input @@ -136,15 +159,60 @@ func waitForTraceeOutput(t *testing.T, gotOutput *eventOutput, now time.Time, fa } } -func waitforTraceeStart(t *testing.T, trc *tracee.Tracee, now time.Time) { +func waitforTraceeStart(t *testing.T, trc *tracee.Tracee) { const checkTimeout = 10 * time.Second + ticker := time.NewTicker(100 * time.Millisecond) + for { - if trc.Running() { - break - } - if time.Since(now) > checkTimeout { + select { + case <-ticker.C: + if trc.Running() { + return + } + case <-time.After(checkTimeout): t.Logf("timed out on running tracee\n") t.FailNow() } } } + +// wait for tracee to stop (or timeout) +// in case of timeout, the test will continue since all tests already passed +func waitforTraceeStop(t *testing.T, trc *tracee.Tracee) { + const checkTimeout = 10 * time.Second + ticker := time.NewTicker(100 * time.Millisecond) + + for { + select { + case <-ticker.C: + if !trc.Running() { + t.Logf("stopped tracee\n") + return + } + case <-time.After(checkTimeout): + t.Logf("timed out on stopping tracee\n") + return + } + } +} + +// 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 + ticker := time.NewTicker(100 * time.Millisecond) + + for { + select { + case <-ticker.C: + if actual.len() >= expectedEvts { + return + } + case <-time.After(checkTimeout): + if failOnTimeout { + t.Logf("timed out on output\n") + t.FailNow() + } + return + } + } +}