From ce696ac95fe3d4f398ec2ed8a8b61c39e29b969e Mon Sep 17 00:00:00 2001 From: Alon Zivony Date: Tue, 19 Sep 2023 16:07:48 +0300 Subject: [PATCH] chore(proctree): add process tree e2e tests Create an end-to-end test for the process tree data source. This in fact test the process tree functionality as a whole. --- .github/workflows/pr.yaml | 2 +- .../e2e-proctree_data_source.go | 304 ++++++++++++++++++ tests/e2e-inst-signatures/export.go | 1 + .../scripts/proctree_data_source.sh | 10 + tests/e2e-inst-test.sh | 1 + 5 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 tests/e2e-inst-signatures/e2e-proctree_data_source.go create mode 100644 tests/e2e-inst-signatures/scripts/proctree_data_source.sh diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 79c5cb2097c2..6bb2b4045703 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -35,7 +35,7 @@ env: # network tests NETTESTS: "IPv4 IPv6 TCP UDP ICMP ICMPv6 DNS HTTP" # instrumentation tests - INSTTESTS: "VFS_WRITE FILE_MODIFICATION SECURITY_INODE_RENAME BPF_ATTACH CONTAINERS_DATA_SOURCE" + INSTTESTS: "VFS_WRITE FILE_MODIFICATION SECURITY_INODE_RENAME BPF_ATTACH CONTAINERS_DATA_SOURCE PROCESS_TREE_DATA_SOURCE" jobs: # # CODE VERIFICATION diff --git a/tests/e2e-inst-signatures/e2e-proctree_data_source.go b/tests/e2e-inst-signatures/e2e-proctree_data_source.go new file mode 100644 index 000000000000..1f5052b25d4b --- /dev/null +++ b/tests/e2e-inst-signatures/e2e-proctree_data_source.go @@ -0,0 +1,304 @@ +package main + +import ( + "fmt" + "reflect" + "time" + + "github.com/aquasecurity/tracee/pkg/utils" + "github.com/aquasecurity/tracee/signatures/helpers" + "github.com/aquasecurity/tracee/types/datasource" + "github.com/aquasecurity/tracee/types/detect" + "github.com/aquasecurity/tracee/types/protocol" + "github.com/aquasecurity/tracee/types/trace" +) + +const ( + bashPath = "/usr/bin/bash" + sleepPath = "/usr/bin/sleep" + lsPath = "/usr/bin/ls" +) + +type e2eProcessTreeDataSource struct { + cb detect.SignatureHandler + processTreeDS detect.DataSource +} + +func (sig *e2eProcessTreeDataSource) Init(ctx detect.SignatureContext) error { + sig.cb = ctx.Callback + processTreeDataSource, ok := ctx.GetDataSource("tracee", "process_tree") + if !ok { + return fmt.Errorf("process tree data source not registered") + } + if processTreeDataSource.Version() > 1 { + return fmt.Errorf("process tree data source version not supported, please update this signature") + } + sig.processTreeDS = processTreeDataSource + return nil +} + +func (sig *e2eProcessTreeDataSource) GetMetadata() (detect.SignatureMetadata, error) { + return detect.SignatureMetadata{ + ID: "PROCESS_TREE_DATA_SOURCE", + EventName: "PROCESS_TREE_DATA_SOURCE", + Version: "0.1.0", + Name: "Process Tree Data Source Test", + Description: "Instrumentation events E2E Tests: Process Tree Data Source Test", + Tags: []string{"e2e", "instrumentation"}, + }, nil +} + +func (sig *e2eProcessTreeDataSource) GetSelectedEvents() ([]detect.SignatureEventSelector, error) { + return []detect.SignatureEventSelector{ + {Source: "tracee", Name: "sched_process_exec", Origin: "*"}, + }, nil +} + +func (sig *e2eProcessTreeDataSource) OnEvent(event protocol.Event) error { + eventObj, ok := event.Payload.(trace.Event) + if !ok { + return fmt.Errorf("failed to cast event's payload") + } + + switch eventObj.EventName { + case "sched_process_exec": + err := sig.checkThread(&eventObj) + if err != nil { + return err + } + + err = sig.checkProcess(&eventObj) + if err != nil { + return err + } + + err = sig.checkLineage(&eventObj) + if err != nil { + return err + } + + m, _ := sig.GetMetadata() + + sig.cb(detect.Finding{ + SigMetadata: m, + Event: event, + Data: map[string]interface{}{}, + }) + } + + return nil +} + +// checkThread check that all the information from the event matches the information in +// the process tree +func (sig *e2eProcessTreeDataSource) checkThread(eventObj *trace.Event) error { + threadHash := utils.HashTaskID(uint32(eventObj.ThreadID), uint64(eventObj.ThreadStartTime)) + threadQueryAnswer, err := sig.processTreeDS.Get( + datasource.ThreadKey{ + Hash: threadHash, + Time: time.Unix(0, int64(eventObj.Timestamp)), + }) + if err != nil { + return fmt.Errorf("failed to find thread in data source: %v", err) + } + + threadInfo, ok := threadQueryAnswer["thread_info"].(datasource.ThreadInfo) + if !ok { + return fmt.Errorf("failed to extract ThreadInfo from data") + } + + // Check IDs + if threadInfo.Tid != eventObj.HostThreadID { + return fmt.Errorf("thread info TID in data source (%d) did not match TID from event (%d)", + threadInfo.Tid, eventObj.HostThreadID) + } + + if threadInfo.NsTid != eventObj.ThreadID { + return fmt.Errorf("thread info NS TID in data source (%d) did not match NS TID from event (%d)", + threadInfo.NsTid, eventObj.ThreadID) + } + + if threadInfo.Pid != eventObj.HostProcessID { + return fmt.Errorf("thread info PID in data source (%d) did not match PID from event (%d)", + threadInfo.Pid, eventObj.HostProcessID) + } + + // Check thread information + if threadInfo.Name != eventObj.ProcessName { + return fmt.Errorf("thread info thread name in data source (%s) did not match known name from event (%s)", + threadInfo.Name, eventObj.ProcessName) + } + return nil +} + +// checkProcess check that the information of the process in this point of time (after +// execution) which we can know from the exec event matches the information of the tree +// (which should process the information before it get to this signature) +func (sig *e2eProcessTreeDataSource) checkProcess(eventObj *trace.Event) error { + procHash, err := sig.getProcessHash(eventObj) + if err != nil { + return err + } + procQueryAnswer, err := sig.processTreeDS.Get( + datasource.ProcKey{ + Hash: procHash, + Time: time.Unix(0, int64(eventObj.Timestamp)), + }) + if err != nil { + return fmt.Errorf("failed to find process in data source: %v", err) + } + + processInfo, ok := procQueryAnswer["process_info"].(datasource.ProcessInfo) + if !ok { + return fmt.Errorf("failed to extract ProcessInfo from data") + } + + // Check IDs + if processInfo.Pid != eventObj.HostProcessID { + return fmt.Errorf("process info PID in data source (%d) did not match PID from event (%d)", + processInfo.Pid, eventObj.HostProcessID) + } + + if processInfo.NsPid != eventObj.ProcessID { + return fmt.Errorf("process info NS PID in data source (%d) did not match NS PID from event (%d)", + processInfo.NsPid, eventObj.ProcessID) + } + + if processInfo.Ppid != eventObj.HostParentProcessID { + return fmt.Errorf("process info PPID in data source (%d) did not match PPID from event (%d)", + processInfo.Ppid, eventObj.HostParentProcessID) + } + + threadExist := false + for tid := range processInfo.ThreadsIds { + if tid == eventObj.HostThreadID { + threadExist = true + break + } + } + if !threadExist { + return fmt.Errorf("process info existing threads (%v) doesn't record current thread (%d)", + processInfo.ThreadsIds, eventObj.HostThreadID) + } + + // Check execution information + pathname, err := helpers.GetTraceeStringArgumentByName(*eventObj, "pathname") + if err != nil { + return err + } + + if pathname != lsPath { + return nil + } + + if processInfo.ExecutionBinary.Path != pathname { + return fmt.Errorf("process info execution binary in data source (%s) did not match known info from event (%s)", + processInfo.ExecutionBinary.Path, pathname) + } + return nil +} + +// checkLineage check that the lineage from the tree matches the information we know from the +// +// // bash script that ran. +func (sig *e2eProcessTreeDataSource) checkLineage(eventObj *trace.Event) error { + // For the check we need only the parent and grandparent (which are created by the test + // script) + maxDepth := 2 + procHash, err := sig.getProcessHash(eventObj) + if err != nil { + return err + } + lineageQueryAnswer, err := sig.processTreeDS.Get( + datasource.LineageKey{ + Hash: procHash, + Time: time.Unix(0, int64(eventObj.Timestamp)), + MaxDepth: maxDepth, + }) + if err != nil { + return fmt.Errorf("failed to find lineage in data source: %v", err) + } + + // We want to check things from the lineage against the process query + procQueryAnswer, err := sig.processTreeDS.Get( + datasource.ProcKey{ + Hash: procHash, + Time: time.Unix(0, int64(eventObj.Timestamp)), + }) + if err != nil { + return fmt.Errorf("failed to find process in data source: %v", err) + } + + processInfo, ok := procQueryAnswer["process_info"].(datasource.ProcessInfo) + if !ok { + return fmt.Errorf("failed to extract ProcessInfo from data") + } + + lineageInfo, ok := lineageQueryAnswer["process_lineage"].(datasource.ProcessLineage) + if !ok { + return fmt.Errorf("failed to extract ProcessLineage from data") + } + expectedLineageLen := maxDepth + 1 // We expect to get all requested ancestors, and the process itself + if len(lineageInfo) != expectedLineageLen { + return fmt.Errorf("missing some ancestors. Expected legacy of %d processes, got %d", expectedLineageLen, len(lineageInfo)) + } + proc := lineageInfo[0] + if !reflect.DeepEqual(proc, processInfo) { + return fmt.Errorf("process in lineage doesn't match process from direct query") + } + + parent := lineageInfo[1] + if parent.ExecutionBinary.Path != bashPath { + return fmt.Errorf("parent process binary path in data source lineage (%s) doesn't match expected (%s)", parent.ExecutionBinary.Path, bashPath) + } + + grandParent := lineageInfo[2] + if grandParent.ExecutionBinary.Path != bashPath { + return fmt.Errorf("grand parent process binary path in data source lineage (%s) doesn't match expected (%s)", grandParent.ExecutionBinary.Path, bashPath) + } + + // Check grandparent info now, which should have changed upon execution of sleep (the + // information in the lineage matches the information upon execution of the parent) + grandParentProcQueryAnswer, err := sig.processTreeDS.Get( + datasource.ProcKey{ + Hash: grandParent.Hash, + Time: time.Unix(0, int64(eventObj.Timestamp)), + }) + if err != nil { + return fmt.Errorf("failed to find process in data source: %v", err) + } + + grandParentCurrentInfo, ok := grandParentProcQueryAnswer["process_info"].(datasource.ProcessInfo) + if !ok { + return fmt.Errorf("failed to extract ProcessInfo from data of grand parent query") + } + + if grandParentCurrentInfo.ExecutionBinary.Path != sleepPath { + return fmt.Errorf("grand parent process binary path in data source (%s) doesn't match expected (%s)", grandParentCurrentInfo.ExecutionBinary.Path, sleepPath) + } + return nil +} + +func (sig *e2eProcessTreeDataSource) OnSignal(s detect.Signal) error { + return nil +} + +func (sig *e2eProcessTreeDataSource) Close() {} + +func (sig *e2eProcessTreeDataSource) getProcessHash(eventObj *trace.Event) (uint32, error) { + threadHash := utils.HashTaskID(uint32(eventObj.ThreadID), uint64(eventObj.ThreadStartTime)) + threadQueryAnswer, err := sig.processTreeDS.Get( + datasource.ThreadKey{ + Hash: threadHash, + Time: time.Unix(0, int64(eventObj.Timestamp)), + }) + if err != nil { + return 0, fmt.Errorf("failed to find thread in data source: %v", err) + } + + threadInfo, ok := threadQueryAnswer["thread_info"].(datasource.ThreadInfo) + if !ok { + return 0, fmt.Errorf("failed to extract ThreadInfo from data") + } + return threadInfo.ProcessHash, nil +} diff --git a/tests/e2e-inst-signatures/export.go b/tests/e2e-inst-signatures/export.go index aa2177e14058..406634198b35 100644 --- a/tests/e2e-inst-signatures/export.go +++ b/tests/e2e-inst-signatures/export.go @@ -9,4 +9,5 @@ var ExportedSignatures = []detect.Signature{ &e2eSecurityInodeRename{}, &e2eContainersDataSource{}, &e2eBpfAttach{}, + &e2eProcessTreeDataSource{}, } diff --git a/tests/e2e-inst-signatures/scripts/proctree_data_source.sh b/tests/e2e-inst-signatures/scripts/proctree_data_source.sh new file mode 100644 index 000000000000..f7c1cb1614ba --- /dev/null +++ b/tests/e2e-inst-signatures/scripts/proctree_data_source.sh @@ -0,0 +1,10 @@ +#!/usr/bin/bash + +exit_err() { + echo -n "ERROR: " + echo $@ + exit 1 +} + +bash -c "bash -c \"bash -c 'sleep 2; ls' & sleep 10\" & exec sleep 10" > /dev/null & +sleep 10 diff --git a/tests/e2e-inst-test.sh b/tests/e2e-inst-test.sh index 03c3ae3f4ea9..b75d2ad7e4bd 100755 --- a/tests/e2e-inst-test.sh +++ b/tests/e2e-inst-test.sh @@ -92,6 +92,7 @@ for TEST in $TESTS; do --log file:$SCRIPT_TMP_DIR/tracee-log-$$ \ --signatures-dir $SIG_DIR \ --scope comm=echo,mv,ls,tracee \ + -- proctree \ --events signatures & # wait tracee-ebpf to be started (30 sec most)