From 9902728669dc208b189375e815fd7c1f2e934bf8 Mon Sep 17 00:00:00 2001 From: Ofek Shaked Date: Sun, 19 May 2024 18:36:51 +0300 Subject: [PATCH 1/4] Added packet context to pcap interface description --- pkg/pcaps/common.go | 19 ++----- pkg/pcaps/interface.go | 121 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 13 deletions(-) create mode 100644 pkg/pcaps/interface.go diff --git a/pkg/pcaps/common.go b/pkg/pcaps/common.go index 1720516ad751..905b93ac4ea6 100644 --- a/pkg/pcaps/common.go +++ b/pkg/pcaps/common.go @@ -2,11 +2,9 @@ package pcaps import ( "fmt" - "math" "os" "strings" - "github.com/google/gopacket/layers" "github.com/google/gopacket/pcapgo" "github.com/aquasecurity/tracee/pkg/config" @@ -17,7 +15,6 @@ import ( ) var outputDirectory *os.File -var fake pcapgo.NgInterface const ( pcapDir string = "pcap/" @@ -43,15 +40,6 @@ const AF_INET6 = 10 func initializeGlobalVars(output *os.File) { outputDirectory = output // where to save pcap files - - // fake interface to be added to each pcap file (needed) - fake = pcapgo.NgInterface{ // https://www.tcpdump.org/linktypes.html - Name: "tracee", - Comment: "trace fake interface", - Description: "non-existing interface", - LinkType: layers.LinkTypeNull, // layer2 is 4 bytes (or 32bit) - SnapLength: uint32(math.MaxUint32), - } } // getItemIndexFromEvent returns correct trace.Event variable according to @@ -207,9 +195,14 @@ func getPcapFileAndWriter(event *trace.Event, t PcapType) ( logger.Debugw("pcap file (re)opened", "filename", pcapFilePath) + iface, err := GenerateInterface(event, t) + if err != nil { + return nil, nil, errfmt.WrapError(err) + } + writer, err := pcapgo.NewNgWriterInterface( file, - fake, + iface, pcapgo.DefaultNgWriterOptions, ) if err != nil { diff --git a/pkg/pcaps/interface.go b/pkg/pcaps/interface.go new file mode 100644 index 000000000000..37815cac5f5f --- /dev/null +++ b/pkg/pcaps/interface.go @@ -0,0 +1,121 @@ +package pcaps + +import ( + "encoding/json" + "math" + + "github.com/aquasecurity/tracee/pkg/errfmt" + "github.com/aquasecurity/tracee/types/trace" + "github.com/google/gopacket/layers" + "github.com/google/gopacket/pcapgo" +) + +// This struct represents the context of a packet capture. +// Packet captures can be per process, command, container or a single capture, +// which affects the context info relevant to it. +// The context contains information that is supposed to be constant for the +// entire capture, although this may not always be the case. +// For example, if a process changes its name, this won't be reflected in the +// capture's context information. +type PacketContext struct { + // Present for container, command and process captures + Container *ContainerContext `json:"container,omitempty"` + Kubernetes *KubernetesContext `json:"kubernetes,omitempty"` + HostName string `json:"hostName,omitempty"` + + // Present for command and process captures + ProcessName string `json:"processName,omitempty"` + + // Present for process captures + Process *ProcessContext `json:"process,omitempty"` +} + +type ProcessContext struct { + ThreadStartTime int `json:"threadStartTime"` + ProcessID int `json:"processId"` + CgroupID uint `json:"cgroupId"` + ThreadID int `json:"threadId"` + ParentProcessID int `json:"parentProcessId"` + HostProcessID int `json:"hostProcessId"` + HostThreadID int `json:"hostThreadId"` + HostParentProcessID int `json:"hostParentProcessId"` + UserID int `json:"userId"` + MountNS int `json:"mountNamespace"` + PIDNS int `json:"pidNamespace"` + Executable string `json:"executable"` +} + +type ContainerContext struct { + ID string `json:"id"` + Name string `json:"name"` + ImageName string `json:"image"` + ImageDigest string `json:"imageDigest"` +} + +type KubernetesContext struct { + PodName string `json:"podName"` + PodNamespace string `json:"podNamespace"` + PodUID string `json:"podUID"` + PodSandbox bool `json:"podSandbox"` +} + +func initPacketContext(event *trace.Event, t PcapType) PacketContext { + ctx := PacketContext{} + + if t == Container || t == Command || t == Process { + ctx.Container = &ContainerContext{ + ID: event.Container.ID, + Name: event.Container.Name, + ImageName: event.Container.ImageName, + ImageDigest: event.Container.ImageDigest, + } + ctx.Kubernetes = &KubernetesContext{ + PodName: event.Kubernetes.PodName, + PodNamespace: event.Kubernetes.PodNamespace, + PodUID: event.Kubernetes.PodUID, + PodSandbox: event.Kubernetes.PodSandbox, + } + ctx.HostName = event.HostName + } + + if t == Command || t == Process { + ctx.ProcessName = event.ProcessName + } + + if t == Process { + ctx.Process = &ProcessContext{ + ThreadStartTime: event.ThreadStartTime, + ProcessID: event.ProcessID, + CgroupID: event.CgroupID, + ThreadID: event.ThreadID, + ParentProcessID: event.ParentProcessID, + HostProcessID: event.HostProcessID, + HostThreadID: event.HostThreadID, + HostParentProcessID: event.HostParentProcessID, + UserID: event.UserID, + MountNS: event.MountNS, + PIDNS: event.PIDNS, + Executable: event.Executable.Path, + } + } + + return ctx +} + +func GenerateInterface(event *trace.Event, t PcapType) (pcapgo.NgInterface, error) { + packetContext := initPacketContext(event, t) + + descBytes, err := json.Marshal(packetContext) + if err != nil { + return pcapgo.NgInterface{}, errfmt.WrapError(err) + } + desc := string(descBytes) + + return pcapgo.NgInterface{ // https://www.tcpdump.org/linktypes.html + Name: "tracee", + Comment: "tracee packet capture", + Description: desc, + LinkType: layers.LinkTypeNull, // layer2 is 4 bytes (or 32bit) + SnapLength: uint32(math.MaxUint32), + }, nil +} From e03556430bda57a3aa2fb615314234654bf31ff0 Mon Sep 17 00:00:00 2001 From: Ofek Shaked Date: Wed, 22 May 2024 14:55:26 +0300 Subject: [PATCH 2/4] Added integration test for packet context --- pkg/pcaps/interface.go | 5 +- tests/integration/capture_test.go | 249 +++++++++++++++++++++++++----- 2 files changed, 216 insertions(+), 38 deletions(-) diff --git a/pkg/pcaps/interface.go b/pkg/pcaps/interface.go index 37815cac5f5f..e9b21f784316 100644 --- a/pkg/pcaps/interface.go +++ b/pkg/pcaps/interface.go @@ -4,10 +4,11 @@ import ( "encoding/json" "math" - "github.com/aquasecurity/tracee/pkg/errfmt" - "github.com/aquasecurity/tracee/types/trace" "github.com/google/gopacket/layers" "github.com/google/gopacket/pcapgo" + + "github.com/aquasecurity/tracee/pkg/errfmt" + "github.com/aquasecurity/tracee/types/trace" ) // This struct represents the context of a packet capture. diff --git a/tests/integration/capture_test.go b/tests/integration/capture_test.go index 0720b8e67796..5cdde7cd0388 100644 --- a/tests/integration/capture_test.go +++ b/tests/integration/capture_test.go @@ -2,17 +2,23 @@ package integration import ( "context" + "encoding/json" "fmt" "os" + "os/exec" + "path" "strings" "syscall" "testing" "time" "unsafe" + "github.com/google/gopacket/pcapgo" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/goleak" + "github.com/aquasecurity/tracee/pkg/pcaps" "github.com/aquasecurity/tracee/tests/testutils" ) @@ -34,36 +40,39 @@ func Test_TraceeCapture(t *testing.T) { pipeReadFilter := fmt.Sprintf("read=%s/pipe*", homeDir) tt := []struct { - name string - coolDown time.Duration - directory string - writeFilter string - readFilter string - test func(t *testing.T, captureDir string, workingDir string) error + name string + coolDown time.Duration + directory string + captureFilters []string + test func(t *testing.T, captureDir string, workingDir string) error }{ { - name: "capture write/read", - coolDown: 0 * time.Second, - directory: "/tmp/tracee/1", - writeFilter: outputWriteFilter, - readFilter: outputReadFilter, - test: readWriteCaptureTest, + name: "capture write/read", + coolDown: 0 * time.Second, + directory: "/tmp/tracee/1", + captureFilters: []string{outputWriteFilter, outputReadFilter}, + test: readWriteCaptureTest, }, { - name: "capture write/readv", - coolDown: 2 * time.Second, - directory: "/tmp/tracee/2", - writeFilter: outputWriteFilter, - readFilter: outputReadFilter, - test: readWritevCaptureTest, + name: "capture write/readv", + coolDown: 2 * time.Second, + directory: "/tmp/tracee/2", + captureFilters: []string{outputWriteFilter, outputReadFilter}, + test: readWritevCaptureTest, }, { - name: "capture pipe write/read", - coolDown: 2 * time.Second, - directory: "/tmp/tracee/3", - writeFilter: pipeWriteFilter, - readFilter: pipeReadFilter, - test: readWritePipe, + name: "capture pipe write/read", + coolDown: 2 * time.Second, + directory: "/tmp/tracee/3", + captureFilters: []string{pipeWriteFilter, pipeReadFilter}, + test: readWritePipe, + }, + { + name: "capture packet context", + coolDown: 0 * time.Second, + directory: "/tmp/tracee/4", + captureFilters: []string{"network", "pcap:single,command,container,process"}, + test: packetContext, }, } @@ -71,7 +80,10 @@ func Test_TraceeCapture(t *testing.T) { for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { coolDown(t, tc.coolDown) - cmd := fmt.Sprintf("--events init_namespaces -c %s -c %s -c dir:%s", tc.readFilter, tc.writeFilter, tc.directory) + cmd := fmt.Sprintf("--events init_namespaces -c dir:%s", tc.directory) + for _, filter := range tc.captureFilters { + cmd = fmt.Sprintf("%s -c %s", cmd, filter) + } running := testutils.NewRunningTracee(context.Background(), cmd) // start tracee @@ -92,7 +104,7 @@ func Test_TraceeCapture(t *testing.T) { var failed bool - captureDir := tc.directory + "/out/host/" + captureDir := path.Join(tc.directory, "out") err := tc.test(t, captureDir, homeDir) if err != nil { failed = true @@ -121,12 +133,8 @@ func Test_TraceeCapture(t *testing.T) { } } -func fileCaptureLocation(captureDir string, inode uint64, dev uint64, oper string) string { - return fmt.Sprintf("%s/%s.dev-%d.inode-%d", captureDir, oper, dev, inode) -} - func readWriteCaptureTest(t *testing.T, captureDir string, workingDir string) error { - outputFile := fmt.Sprintf("%s/output.txt", workingDir) + outputFile := path.Join(workingDir, "output.txt") const input string = "Hello World\n" file, err := os.Create(outputFile) if err != nil { @@ -170,7 +178,7 @@ func readWriteCaptureTest(t *testing.T, captureDir string, workingDir string) er } func readWritevCaptureTest(t *testing.T, captureDir string, workingDir string) error { - outputFile := fmt.Sprintf("%s/output.txt", workingDir) + outputFile := path.Join(workingDir, "output.txt") file, err := os.Create(outputFile) if err != nil { return err @@ -245,7 +253,7 @@ func readWritevCaptureTest(t *testing.T, captureDir string, workingDir string) e } func readWritePipe(t *testing.T, captureDir string, workingDir string) error { - namedPipe := fmt.Sprintf("%s/pipe_test", workingDir) + namedPipe := path.Join(workingDir, "pipe_test") err := os.Remove(namedPipe) if err != nil && !os.IsNotExist(err) { return err @@ -307,7 +315,9 @@ func assertEntries(t *testing.T, captureDir string, input string, readOut string // Ensure capture files are generated coolDown(t, 5*time.Second) - entries, err := os.ReadDir(captureDir) + hostCaptureDir := path.Join(captureDir, "host") + + entries, err := os.ReadDir(hostCaptureDir) if err != nil { return err } @@ -329,13 +339,13 @@ func assertEntries(t *testing.T, captureDir string, input string, readOut string continue } if strings.HasPrefix(entryName, "read") { - readCaptureFile, err = os.ReadFile(captureDir + entryName) + readCaptureFile, err = os.ReadFile(path.Join(hostCaptureDir, entryName)) if err != nil { return err } found++ } else if strings.HasPrefix(entryName, "write") { - writeCaptureFile, err = os.ReadFile(captureDir + entryName) + writeCaptureFile, err = os.ReadFile(path.Join(hostCaptureDir, entryName)) if err != nil { return err } @@ -359,3 +369,170 @@ func assertEntries(t *testing.T, captureDir string, input string, readOut string return nil } + +func getPacketContext(pcapFile string) (pcaps.PacketContext, error) { + packetContext := pcaps.PacketContext{} + + reader, err := os.Open(pcapFile) + if err != nil { + return packetContext, err + } + pcapReader, err := pcapgo.NewNgReader(reader, pcapgo.DefaultNgReaderOptions) + if err != nil { + return packetContext, err + } + + // Get the first interface + iface, err := pcapReader.Interface(0) + if err != nil { + return packetContext, err + } + + // Unmarshal the interface description JSON to PacketContext + err = json.Unmarshal([]byte(iface.Description), &packetContext) + return packetContext, err +} + +func findProcessPcapFile(dir string, processName string) (string, error) { + entries, err := os.ReadDir(dir) + if err != nil { + return "", err + } + var pcap string + found := false + for _, entry := range entries { + if strings.HasPrefix(entry.Name(), "ping_") && strings.HasSuffix(entry.Name(), ".pcap") { + if found { + return "", fmt.Errorf("found multiple pcap files for process %s", processName) + } + pcap = entry.Name() + found = true + } + } + if !found { + return "", fmt.Errorf("could not find ping pcap") + } + + return pcap, nil +} + +func assertContext(t *testing.T, pcapFile string, pcapType pcaps.PcapType, hostName *string, containerId *string, processName *string, processID *int) error { + packetContext, err := getPacketContext(pcapFile) + if err != nil { + return err + } + + // Single pcap should have empty context + if pcapType == pcaps.Single { + assert.Nil(t, packetContext.Container) + assert.Nil(t, packetContext.Kubernetes) + assert.Equal(t, "", packetContext.HostName) + assert.Equal(t, "", packetContext.ProcessName) + assert.Nil(t, packetContext.Process) + return nil + } + + // Container, command and process pcaps should have container, kubernetes and hostname info + if pcapType == pcaps.Container || pcapType == pcaps.Command || pcapType == pcaps.Process { + assert.NotNil(t, packetContext.Container) + assert.Equal(t, *containerId, packetContext.Container.ID) + assert.NotNil(t, packetContext.Kubernetes) + // TODO: test kubernetes info validity + assert.Equal(t, *hostName, packetContext.HostName) + } + + // Command and process pcaps should have process name + if pcapType == pcaps.Command || pcapType == pcaps.Process { + assert.Equal(t, *processName, packetContext.ProcessName) + } else { + assert.Equal(t, "", packetContext.ProcessName) + } + + // Process pcaps should have process info + if pcapType == pcaps.Process { + assert.NotNil(t, packetContext.Process) + assert.Equal(t, *processID, packetContext.Process.ProcessID) + } else { + assert.Nil(t, packetContext.Process) + } + + return nil +} + +func packetContext(t *testing.T, captureDir string, workingDir string) error { + var emptyString = "" + var ping = "ping" + + pcapDir := path.Join(captureDir, "pcap") + hostName, err := os.Hostname() + if err != nil { + return err + } + + // Ping localhost from host + cmd := exec.Command("ping", "-c", "1", "127.0.0.1") + if err := cmd.Run(); err != nil { + return err + } + pid := cmd.Process.Pid + + // Ping localhost from a container (use busybox because it's smaller than alpine) + cmd = exec.Command("docker", "run", "-d", "--rm", "busybox", "ping", "-c", "1", "127.0.0.1") + // Get the container ID from the output + output, err := cmd.Output() + if err != nil { + return err + } + containerId := strings.TrimSuffix(string(output), "\n") + containerHostname := containerId[0:12] + + // Ensure packets are written to pcap files + coolDown(t, 5*time.Second) + + // Test context of single pcap + err = assertContext(t, path.Join(pcapDir, "single.pcap"), pcaps.Single, nil, nil, nil, nil) + if err != nil { + return err + } + + // Test context of container pcaps + err = assertContext(t, path.Join(pcapDir, "containers", "host.pcap"), pcaps.Container, &hostName, &emptyString, nil, nil) + if err != nil { + return err + } + err = assertContext(t, path.Join(pcapDir, "containers", fmt.Sprintf("%s.pcap", containerId[0:11])), pcaps.Container, &containerHostname, &containerId, nil, nil) + if err != nil { + return err + } + + // Test context of command pcaps + err = assertContext(t, path.Join(pcapDir, "commands", "host", "ping.pcap"), pcaps.Command, &hostName, &emptyString, &ping, nil) + if err != nil { + return err + } + err = assertContext(t, path.Join(pcapDir, "commands", containerId[0:11], "ping.pcap"), pcaps.Command, &containerHostname, &containerId, &ping, nil) + if err != nil { + return err + } + + // Test context of process pcaps + pcapFile, err := findProcessPcapFile(path.Join(pcapDir, "processes", "host"), "ping") + if err != nil { + return err + } + err = assertContext(t, path.Join(pcapDir, "processes", "host", pcapFile), pcaps.Process, &hostName, &emptyString, &ping, &pid) + if err != nil { + return err + } + pcapFile, err = findProcessPcapFile(path.Join(pcapDir, "processes", containerId[0:11]), "ping") + if err != nil { + return err + } + var one = 1 + err = assertContext(t, path.Join(pcapDir, "processes", containerId[0:11], pcapFile), pcaps.Process, &containerHostname, &containerId, &ping, &one) + if err != nil { + return err + } + + return nil +} From 82397d8cfa45d2dc3235bbbdd575232120fab047 Mon Sep 17 00:00:00 2001 From: Ofek Shaked Date: Mon, 24 Jun 2024 09:55:37 +0300 Subject: [PATCH 3/4] Add version to packet context --- pkg/pcaps/interface.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/pcaps/interface.go b/pkg/pcaps/interface.go index e9b21f784316..241b3e673b1d 100644 --- a/pkg/pcaps/interface.go +++ b/pkg/pcaps/interface.go @@ -11,6 +11,8 @@ import ( "github.com/aquasecurity/tracee/types/trace" ) +var packetContextVersion = "1.0" + // This struct represents the context of a packet capture. // Packet captures can be per process, command, container or a single capture, // which affects the context info relevant to it. @@ -19,6 +21,8 @@ import ( // For example, if a process changes its name, this won't be reflected in the // capture's context information. type PacketContext struct { + Version string `json:"version"` + // Present for container, command and process captures Container *ContainerContext `json:"container,omitempty"` Kubernetes *KubernetesContext `json:"kubernetes,omitempty"` @@ -61,7 +65,7 @@ type KubernetesContext struct { } func initPacketContext(event *trace.Event, t PcapType) PacketContext { - ctx := PacketContext{} + ctx := PacketContext{Version: packetContextVersion} if t == Container || t == Command || t == Process { ctx.Container = &ContainerContext{ From 8cd4dfee92487989870ba908dda18b516364db6f Mon Sep 17 00:00:00 2001 From: Ofek Shaked Date: Mon, 24 Jun 2024 15:25:12 +0300 Subject: [PATCH 4/4] Add `omitempty` to container and k8s fields --- pkg/pcaps/interface.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/pcaps/interface.go b/pkg/pcaps/interface.go index 241b3e673b1d..44970efa1ca8 100644 --- a/pkg/pcaps/interface.go +++ b/pkg/pcaps/interface.go @@ -51,17 +51,17 @@ type ProcessContext struct { } type ContainerContext struct { - ID string `json:"id"` - Name string `json:"name"` - ImageName string `json:"image"` - ImageDigest string `json:"imageDigest"` + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + ImageName string `json:"image,omitempty"` + ImageDigest string `json:"imageDigest,omitempty"` } type KubernetesContext struct { - PodName string `json:"podName"` - PodNamespace string `json:"podNamespace"` - PodUID string `json:"podUID"` - PodSandbox bool `json:"podSandbox"` + PodName string `json:"podName,omitempty"` + PodNamespace string `json:"podNamespace,omitempty"` + PodUID string `json:"podUID,omitempty"` + PodSandbox bool `json:"podSandbox,omitempty"` } func initPacketContext(event *trace.Event, t PcapType) PacketContext {