From cb01f5914302cdee050cc624e43f2cd3621bca31 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 24 Jan 2024 08:42:38 +0200 Subject: [PATCH 01/39] feat: add helper funcs to get symbol info from /proc/kallsyms --- .../module/file_integrity/kprobes/errors.go | 24 +++++ .../module/file_integrity/kprobes/kallsyms.go | 95 +++++++++++++++++++ .../file_integrity/kprobes/kallsyms_test.go | 74 +++++++++++++++ 3 files changed, 193 insertions(+) create mode 100644 auditbeat/module/file_integrity/kprobes/errors.go create mode 100644 auditbeat/module/file_integrity/kprobes/kallsyms.go create mode 100644 auditbeat/module/file_integrity/kprobes/kallsyms_test.go diff --git a/auditbeat/module/file_integrity/kprobes/errors.go b/auditbeat/module/file_integrity/kprobes/errors.go new file mode 100644 index 00000000000..e42192bfe4c --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/errors.go @@ -0,0 +1,24 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import "errors" + +var ( + ErrSymbolNotFound = errors.New("symbol not found") +) diff --git a/auditbeat/module/file_integrity/kprobes/kallsyms.go b/auditbeat/module/file_integrity/kprobes/kallsyms.go new file mode 100644 index 00000000000..fd146660246 --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/kallsyms.go @@ -0,0 +1,95 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//go:build linux + +package kprobes + +import ( + "bufio" + "fmt" + "io" + "os" + "regexp" + "strings" +) + +const kAllSymsPath = "/proc/kallsyms" + +type runtimeSymbolInfo struct { + symbolName string + isOptimised bool + optimisedSymbolName string +} + +// getSymbolInfoRuntime returns the runtime symbol information for the given symbolName +// from the /proc/kallsyms file. +func getSymbolInfoRuntime(symbolName string) (runtimeSymbolInfo, error) { + kAllSymsFile, err := os.Open(kAllSymsPath) + if err != nil { + return runtimeSymbolInfo{}, err + } + + defer func() { + _ = kAllSymsFile.Close() + }() + + return getSymbolInfoFromReader(kAllSymsFile, symbolName) +} + +// getSymbolInfoFromReader retrieves symbol information from a reader that is expected to +// provide content in the same format as /proc/kallsyms +func getSymbolInfoFromReader(reader io.Reader, symbolName string) (runtimeSymbolInfo, error) { + fileScanner := bufio.NewScanner(reader) + fileScanner.Split(bufio.ScanLines) + + symReg, err := regexp.Compile(fmt.Sprintf("(?m)^([a-fA-F0-9]+).*?(%s(|.*?)?)(\\s+.*$|$)", symbolName)) + if err != nil { + return runtimeSymbolInfo{}, err + } + + // optimised symbols start with the unoptimised symbol name + // followed by ".{optimisation_type}..." + optimisedSymbolName := symbolName + "." + + for fileScanner.Scan() { + matches := symReg.FindAllSubmatch(fileScanner.Bytes(), -1) + if len(matches) == 0 { + continue + } + + for _, match := range matches { + matchSymbolName := string(match[2]) + switch { + case strings.HasPrefix(matchSymbolName, optimisedSymbolName): + return runtimeSymbolInfo{ + symbolName: symbolName, + isOptimised: true, + optimisedSymbolName: matchSymbolName, + }, nil + case strings.EqualFold(matchSymbolName, symbolName): + return runtimeSymbolInfo{ + symbolName: symbolName, + isOptimised: false, + optimisedSymbolName: "", + }, nil + } + } + } + + return runtimeSymbolInfo{}, ErrSymbolNotFound +} diff --git a/auditbeat/module/file_integrity/kprobes/kallsyms_test.go b/auditbeat/module/file_integrity/kprobes/kallsyms_test.go new file mode 100644 index 00000000000..d60757afc0f --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/kallsyms_test.go @@ -0,0 +1,74 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_getSymbolInfoFromReader(t *testing.T) { + + content := `0000000000000000 t fsnotify_move +0000000000000000 T fsnotify +0000000000000000 T fsnotifyy +0000000000000000 t fsnotify_file.isra.0 [btrfs] +0000000000000000 t chmod_common.isra.0` + + cases := []struct { + tName string + symbolName string + isOptimised bool + optimisedSymbolName string + err error + }{ + { + tName: "symbol_exists", + symbolName: "fsnotify", + isOptimised: false, + optimisedSymbolName: "", + err: nil, + }, + { + tName: "symbol_exists_optimised", + symbolName: "chmod_common", + isOptimised: true, + optimisedSymbolName: "chmod_common.isra.0", + err: nil, + }, + { + tName: "symbol_exists_optimised_with_space_at_end", + symbolName: "fsnotify_file", + isOptimised: true, + optimisedSymbolName: "fsnotify_file.isra.0", + err: nil, + }, + } + + for _, tc := range cases { + t.Run(tc.tName, func(t *testing.T) { + symInfo, err := getSymbolInfoFromReader(strings.NewReader(content), tc.symbolName) + require.IsType(t, err, tc.err) + require.Equal(t, tc.symbolName, symInfo.symbolName) + require.Equal(t, tc.isOptimised, symInfo.isOptimised) + require.Equal(t, tc.optimisedSymbolName, symInfo.optimisedSymbolName) + }) + } +} From 94172438dec711e137e6f12355fc29171e23df73 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 24 Jan 2024 09:09:36 +0200 Subject: [PATCH 02/39] feat: introduce fixed executor that always runs funcs from the same os thread --- .../module/file_integrity/kprobes/executor.go | 126 ++++++++++++++++ .../file_integrity/kprobes/executor_test.go | 136 ++++++++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 auditbeat/module/file_integrity/kprobes/executor.go create mode 100644 auditbeat/module/file_integrity/kprobes/executor_test.go diff --git a/auditbeat/module/file_integrity/kprobes/executor.go b/auditbeat/module/file_integrity/kprobes/executor.go new file mode 100644 index 00000000000..c026644569e --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/executor.go @@ -0,0 +1,126 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "context" + "runtime" + + "golang.org/x/sys/unix" +) + +type executor interface { + Run(f func() error) error + GetTID() int +} + +// fixedExecutor runs tasks on a fixed OS thread (see runtime.LockOSThread). +type fixedExecutor struct { + ctx context.Context + cancelFn context.CancelFunc + // tid is the OS identifier for the thread where it is running. + tid int + runC chan func() error + retC chan error +} + +// Run submits new tasks to run on the executor and waits for them to finish returning any error. +func (ex *fixedExecutor) Run(f func() error) error { + if ctxErr := ex.ctx.Err(); ctxErr != nil { + return ctxErr + } + + select { + case ex.runC <- f: + case <-ex.ctx.Done(): + return ex.ctx.Err() + } + + select { + case <-ex.ctx.Done(): + return ex.ctx.Err() + case err := <-ex.retC: + return err + } +} + +// GetTID returns the OS identifier for the thread where executor goroutine is locked against. +func (ex *fixedExecutor) GetTID() int { + return ex.tid +} + +// Close terminates the executor. Pending tasks will still be run. +func (ex *fixedExecutor) Close() { + ex.cancelFn() + close(ex.runC) +} + +// newFixedThreadExecutor returns a new fixedExecutor. +func newFixedThreadExecutor(ctx context.Context) *fixedExecutor { + + mCtx, cancelFn := context.WithCancel(ctx) + + ex := &fixedExecutor{ + ctx: mCtx, + cancelFn: cancelFn, + runC: make(chan func() error, 1), + retC: make(chan error, 1), + } + + tidC := make(chan int) + + go func() { + defer close(ex.retC) + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + select { + case <-ctx.Done(): + return + case tidC <- unix.Gettid(): + close(tidC) + } + + for { + select { + case runF, ok := <-ex.runC: + if !ok { + // channel closed + return + } + + select { + case ex.retC <- runF(): + case <-ex.ctx.Done(): + return + } + + case <-ex.ctx.Done(): + return + } + } + }() + + select { + case ex.tid = <-tidC: + case <-ctx.Done(): + return nil + } + + return ex +} diff --git a/auditbeat/module/file_integrity/kprobes/executor_test.go b/auditbeat/module/file_integrity/kprobes/executor_test.go new file mode 100644 index 00000000000..3083ccbb026 --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/executor_test.go @@ -0,0 +1,136 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "context" + "errors" + "runtime" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func Test_executor(t *testing.T) { + + if runtime.GOOS != "linux" { + t.Skip("skipping on non-linux") + } + + // parent context is cancelled at creation + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + exec := newFixedThreadExecutor(ctx) + require.Nil(t, exec) + + // parent context is cancelled + ctx, cancel = context.WithCancel(context.Background()) + exec = newFixedThreadExecutor(ctx) + require.NotNil(t, exec) + + err := exec.Run(func() error { + cancel() + time.Sleep(10 * time.Second) + return nil + }) + require.ErrorIs(t, err, ctx.Err()) + require.ErrorIs(t, exec.Run(func() error { + return nil + }), ctx.Err()) + + // executor is closed while running cancelled + exec = newFixedThreadExecutor(context.Background()) + require.NotNil(t, exec) + + err = exec.Run(func() error { + exec.Close() + time.Sleep(10 * time.Second) + return nil + }) + require.ErrorIs(t, err, exec.ctx.Err()) + + // normal exec no error + exec = newFixedThreadExecutor(context.Background()) + require.NotNil(t, exec) + + err = exec.Run(func() error { + time.Sleep(1 * time.Second) + return nil + }) + require.NoError(t, err) + exec.Close() + + // exec with error + exec = newFixedThreadExecutor(context.Background()) + require.NotNil(t, exec) + retErr := errors.New("test error") + + err = exec.Run(func() error { + return retErr + }) + require.ErrorIs(t, err, retErr) + exec.Close() + + // check that runs are indeed sequential + // as pathTraverser depends on it + err = nil + atomicInt := uint32(0) + atomicCheck := func() error { + swapped := atomic.CompareAndSwapUint32(&atomicInt, 0, 1) + if !swapped { + return errors.New("parallel runs") + } + time.Sleep(1 * time.Second) + swapped = atomic.CompareAndSwapUint32(&atomicInt, 1, 0) + if !swapped { + return errors.New("parallel runs") + } + return nil + } + exec = newFixedThreadExecutor(context.Background()) + require.NotNil(t, exec) + errChannel := make(chan error, 1) + wg := sync.WaitGroup{} + start := make(chan struct{}) + for i := 0; i < 4; i++ { + wg.Add(1) + go func() { + defer wg.Done() + <-start + if runErr := exec.Run(atomicCheck); runErr != nil { + select { + case errChannel <- runErr: + default: + } + } + }() + } + time.Sleep(1 * time.Second) + close(start) + wg.Wait() + select { + case err = <-errChannel: + default: + + } + require.Nil(t, err) +} From bbca9e7dba4c1f79b6acb57d63d6101c5bcbdc94 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 24 Jan 2024 09:14:06 +0200 Subject: [PATCH 03/39] feat: add probe manager to handle building tracing kprobes from tk-btf ones --- .../module/file_integrity/kprobes/probes.go | 104 ++++++++++++++++++ go.mod | 2 + go.sum | 11 +- 3 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 auditbeat/module/file_integrity/kprobes/probes.go diff --git a/auditbeat/module/file_integrity/kprobes/probes.go b/auditbeat/module/file_integrity/kprobes/probes.go new file mode 100644 index 00000000000..126dae2c9dd --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/probes.go @@ -0,0 +1,104 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "github.com/elastic/beats/v7/auditbeat/tracing" + + tkbtf "github.com/elastic/tk-btf" +) + +type probeWithAllocFunc struct { + probe *tkbtf.Probe + allocateFn func() any +} + +type shouldBuildCheck func(spec *tkbtf.Spec) bool + +type symbol interface { + buildProbes(spec *tkbtf.Spec) ([]*probeWithAllocFunc, error) + + onErr(err error) bool +} + +type probeManager struct { + symbols []symbol + buildChecks []shouldBuildCheck + getSymbolInfoRuntime func(symbolName string) (runtimeSymbolInfo, error) +} + +func newProbeManager(e executor) (*probeManager, error) { + fs := &probeManager{ + symbols: nil, + buildChecks: nil, + getSymbolInfoRuntime: getSymbolInfoRuntime, + } + + return fs, nil +} + +func (fs *probeManager) shouldBuild(spec *tkbtf.Spec) bool { + for _, check := range fs.buildChecks { + if !check(spec) { + return false + } + } + + return true +} + +func (fs *probeManager) build(spec *tkbtf.Spec) (map[tracing.Probe]tracing.AllocateFn, error) { + trProbesMap := make(map[tracing.Probe]tracing.AllocateFn) + + for _, s := range fs.symbols { + probesWithAlloc, err := s.buildProbes(spec) + if err != nil { + return nil, err + } + + for _, p := range probesWithAlloc { + trProbe := tracing.Probe{ + Group: "auditbeat_fim", + Name: p.probe.GetID(), + Address: p.probe.GetSymbolName(), + Fetchargs: p.probe.GetTracingEventProbe(), + Filter: p.probe.GetTracingEventFilter(), + } + switch p.probe.GetType() { + case tkbtf.ProbeTypeKRetProbe: + trProbe.Type = tracing.TypeKRetProbe + default: + trProbe.Type = tracing.TypeKProbe + } + trProbesMap[trProbe] = p.allocateFn + } + } + + return trProbesMap, nil +} + +func (fs *probeManager) onErr(err error) bool { + repeat := false + for _, s := range fs.symbols { + if s.onErr(err) { + repeat = true + } + } + + return repeat +} diff --git a/go.mod b/go.mod index 462f575b24f..30065acb945 100644 --- a/go.mod +++ b/go.mod @@ -206,6 +206,7 @@ require ( github.com/elastic/elastic-agent-system-metrics v0.9.1 github.com/elastic/go-elasticsearch/v8 v8.11.1 github.com/elastic/mito v1.8.0 + github.com/elastic/tk-btf v0.1.0 github.com/elastic/toutoumomoma v0.0.0-20221026030040-594ef30cb640 github.com/foxcpp/go-mockdns v0.0.0-20201212160233-ede2f9158d15 github.com/google/cel-go v0.19.0 @@ -265,6 +266,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.5 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash v1.1.0 // indirect + github.com/cilium/ebpf v0.12.3 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect diff --git a/go.sum b/go.sum index fca63002617..0e11a96555f 100644 --- a/go.sum +++ b/go.sum @@ -430,6 +430,8 @@ github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLI github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= +github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4= +github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= @@ -709,6 +711,8 @@ github.com/elastic/ristretto v0.1.1-0.20220602190459-83b0895ca5b3 h1:ChPwRVv1RR4 github.com/elastic/ristretto v0.1.1-0.20220602190459-83b0895ca5b3/go.mod h1:RAy2GVV4sTWVlNMavv3xhLsk18rxhfhDnombTe6EF5c= github.com/elastic/sarama v1.19.1-0.20220310193331-ebc2b0d8eef3 h1:FzA0/n4iMt8ojGDGRoiFPSHFvvdVIvxOxyLtiFnrLBM= github.com/elastic/sarama v1.19.1-0.20220310193331-ebc2b0d8eef3/go.mod h1:mdtqvCSg8JOxk8PmpTNGyo6wzd4BMm4QXSfDnTXmgkE= +github.com/elastic/tk-btf v0.1.0 h1:T4rbsnfaRH/MZKSLwZFd3sndt/NexsQb0IXWtMQ9PAA= +github.com/elastic/tk-btf v0.1.0/go.mod h1:caLQPEcMbyKmPUQb2AsbX3ZAj1yCz06lOxfhn0esLR8= github.com/elastic/toutoumomoma v0.0.0-20221026030040-594ef30cb640 h1:oJbI/v6q/PDOZrsruajnbbt7mujobOPDUmkePcVMkJA= github.com/elastic/toutoumomoma v0.0.0-20221026030040-594ef30cb640/go.mod h1:C26fjgblYUZyl9aRc0D4piK8WqQzeCwUdIvjN5OsTnY= github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= @@ -755,8 +759,8 @@ github.com/foxcpp/go-mockdns v0.0.0-20201212160233-ede2f9158d15/go.mod h1:tPg4cp github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= -github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= +github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M= github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= @@ -1341,8 +1345,9 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= From 292b7b7e89237b6a0868823cabfb687f883d3aae Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 24 Jan 2024 09:16:46 +0200 Subject: [PATCH 04/39] feat: define probe events with corresponding alloc and release funcs --- .../module/file_integrity/kprobes/events.go | 140 ++++++++++++++++++ .../file_integrity/kprobes/events_test.go | 107 +++++++++++++ 2 files changed, 247 insertions(+) create mode 100644 auditbeat/module/file_integrity/kprobes/events.go create mode 100644 auditbeat/module/file_integrity/kprobes/events_test.go diff --git a/auditbeat/module/file_integrity/kprobes/events.go b/auditbeat/module/file_integrity/kprobes/events.go new file mode 100644 index 00000000000..018f392b717 --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/events.go @@ -0,0 +1,140 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "sync" + + "github.com/elastic/beats/v7/auditbeat/tracing" +) + +var probeEventPool = sync.Pool{ + New: func() interface{} { + return &ProbeEvent{} + }, +} + +// ProbeEvent represents a kprobe event. +// Different Mask* fields represent different kind of events. +// For MaskMonitor, the fields that are filled are: +// +// ParentIno, ParentDevMajor, ParentDevMinor, FileIno, FileDevMajor, FileDevMinor, FileName +// +// For MaskCreate, the fields that are filled are: +// +// ParentIno, ParentDevMajor, ParentDevMinor, FileIno, FileDevMajor, FileDevMinor, FileName +// +// For MaskDelete, the fields that are filled are: +// +// ParentIno, ParentDevMajor, ParentDevMinor, FileName +// +// For MaskModify, the fields that are filled are: +// +// FileIno, FileDevMajor, FileDevMinor +// +// For MaskAttrib, the fields that are filled are: +// +// FileIno, FileDevMajor, FileDevMinor +// +// For MaskMoveTo, the fields that are filled are: +// +// ParentIno, ParentDevMajor, ParentDevMinor, FileName +// +// For MaskMoveFrom, the fields that are filled are: +// +// ParentIno, ParentDevMajor, ParentDevMinor, FileName +// +// The reason that we opted for one Type (aka ProbeEvent struct) to capture different events as +// inner fields is to utilise the same sync.Pool. As events are eventually generated by any +// process on the system, a storm of events can easily occur, avoiding constant allocations +// should benefit the performance of garbage collector. +type ProbeEvent struct { + Meta tracing.Metadata `kprobe:"metadata"` + MaskMonitor uint32 + MaskCreate uint32 `kprobe:"mc,allowundefined"` + MaskDelete uint32 `kprobe:"md,allowundefined"` + MaskAttrib uint32 `kprobe:"ma,allowundefined"` + MaskModify uint32 `kprobe:"mm,allowundefined"` + MaskDir uint32 `kprobe:"mid,allowundefined"` + MaskMoveTo uint32 `kprobe:"mmt,allowundefined"` + MaskMoveFrom uint32 `kprobe:"mmf,allowundefined"` + ParentIno uint64 `kprobe:"pi"` + ParentDevMajor uint32 `kprobe:"pdmj"` + ParentDevMinor uint32 `kprobe:"pdmn"` + FileIno uint64 `kprobe:"fi"` + FileDevMajor uint32 `kprobe:"fdmj"` + FileDevMinor uint32 `kprobe:"fdmn"` + FileName string `kprobe:"fn"` +} + +// allocProbeEvent gets a ProbeEvent from the sync.Pool and zero it out. Note that depending on the +// pool state an allocation might happen. +func allocProbeEvent() any { + probeEvent := probeEventPool.Get().(*ProbeEvent) + // zero out all Mask related fields + probeEvent.MaskMonitor = 0 + probeEvent.MaskCreate = 0 + probeEvent.MaskDelete = 0 + probeEvent.MaskAttrib = 0 + probeEvent.MaskModify = 0 + probeEvent.MaskDir = 0 + probeEvent.MaskMoveTo = 0 + probeEvent.MaskMoveFrom = 0 + return probeEvent +} + +// allocDeleteProbeEvent gets a ProbeEvent from the sync.Pool and zero it out except for MaskDelete. +// Note that depending on the pool state an allocation might happen. +func allocDeleteProbeEvent() any { + probeEvent := probeEventPool.Get().(*ProbeEvent) + // zero out all Mask related fields except MaskDelete + probeEvent.MaskMonitor = 0 + probeEvent.MaskCreate = 0 + probeEvent.MaskDelete = 1 + probeEvent.MaskAttrib = 0 + probeEvent.MaskModify = 0 + probeEvent.MaskDir = 0 + probeEvent.MaskMoveTo = 0 + probeEvent.MaskMoveFrom = 0 + return probeEvent +} + +// allocMonitorProbeEvent gets a ProbeEvent from the sync.Pool and zero it out except for MaskMonitor. +// Note that depending on the pool state an allocation might happen. +func allocMonitorProbeEvent() any { + probeEvent := probeEventPool.Get().(*ProbeEvent) + // zero out all Mask related fields except MaskMonitor + probeEvent.MaskMonitor = 1 + probeEvent.MaskCreate = 0 + probeEvent.MaskDelete = 0 + probeEvent.MaskAttrib = 0 + probeEvent.MaskModify = 0 + probeEvent.MaskDir = 0 + probeEvent.MaskMoveTo = 0 + probeEvent.MaskMoveFrom = 0 + return probeEvent +} + +// releaseProbeEvent returns a ProbeEvent to the pool. +func releaseProbeEvent(c *ProbeEvent) { + if c == nil { + return + } + + probeEventPool.Put(c) +} diff --git a/auditbeat/module/file_integrity/kprobes/events_test.go b/auditbeat/module/file_integrity/kprobes/events_test.go new file mode 100644 index 00000000000..a898aecd395 --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/events_test.go @@ -0,0 +1,107 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_allocProbeEvents(t *testing.T) { + p := allocProbeEvent() + require.IsType(t, &ProbeEvent{}, p) + + releaseProbeEvent(nil) + + pE := p.(*ProbeEvent) + require.Zero(t, pE.MaskMonitor) + require.Zero(t, pE.MaskCreate) + require.Zero(t, pE.MaskDelete) + require.Zero(t, pE.MaskAttrib) + require.Zero(t, pE.MaskModify) + require.Zero(t, pE.MaskDir) + require.Zero(t, pE.MaskMoveTo) + require.Zero(t, pE.MaskMoveFrom) + releaseProbeEvent(pE) + + p = allocDeleteProbeEvent() + require.IsType(t, &ProbeEvent{}, p) + + pE = p.(*ProbeEvent) + require.Zero(t, pE.MaskMonitor) + require.Zero(t, pE.MaskCreate) + require.Equal(t, pE.MaskDelete, uint32(1)) + require.Zero(t, pE.MaskAttrib) + require.Zero(t, pE.MaskModify) + require.Zero(t, pE.MaskDir) + require.Zero(t, pE.MaskMoveTo) + require.Zero(t, pE.MaskMoveFrom) + releaseProbeEvent(pE) + + p = allocMonitorProbeEvent() + require.IsType(t, &ProbeEvent{}, p) + + pE = p.(*ProbeEvent) + require.Equal(t, pE.MaskMonitor, uint32(1)) + require.Zero(t, pE.MaskCreate) + require.Zero(t, pE.MaskDelete) + require.Zero(t, pE.MaskAttrib) + require.Zero(t, pE.MaskModify) + require.Zero(t, pE.MaskDir) + require.Zero(t, pE.MaskMoveTo) + require.Zero(t, pE.MaskMoveFrom) + releaseProbeEvent(pE) +} + +func BenchmarkEventAllocation(b *testing.B) { + var p *ProbeEvent + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + for j := 0; j < 10000; j++ { + p = &ProbeEvent{} + _ = p + p = &ProbeEvent{MaskMonitor: 1} + _ = p + p = &ProbeEvent{MaskDelete: 1} + _ = p + } + } + _ = p +} + +func BenchmarkEventPool(b *testing.B) { + var p any + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + for j := 0; j < 10000; j++ { + p = allocProbeEvent() + _ = p + releaseProbeEvent(p.(*ProbeEvent)) + p = allocMonitorProbeEvent() + _ = p + releaseProbeEvent(p.(*ProbeEvent)) + p = allocDeleteProbeEvent() + _ = p + releaseProbeEvent(p.(*ProbeEvent)) + } + } + _ = p +} From d585da15bade29bcc6527ddabeacda2567592cd0 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 24 Jan 2024 09:20:24 +0200 Subject: [PATCH 05/39] feat: embed stripped btf files and add helper funcs to read them --- .../embed/3.10.0-1062.1.1.el7.x86_64.btf | Bin 0 -> 1666 bytes .../embed/4.14.101-91.76.amzn2.aarch64.btf | Bin 0 -> 1497 bytes .../embed/4.18.0-193.1.2.el7.aarch64.btf | Bin 0 -> 1626 bytes .../embed/5.10.0-0.deb10.17-rt-arm64.btf | Bin 0 -> 1334 bytes .../embed/5.10.109-200.el7.aarch64.btf | Bin 0 -> 1334 bytes .../kprobes/embed/5.8.0-1035-aws.btf | Bin 0 -> 1336 bytes .../module/file_integrity/kprobes/verifier.go | 85 ++++++++++++++++++ 7 files changed, 85 insertions(+) create mode 100644 auditbeat/module/file_integrity/kprobes/embed/3.10.0-1062.1.1.el7.x86_64.btf create mode 100644 auditbeat/module/file_integrity/kprobes/embed/4.14.101-91.76.amzn2.aarch64.btf create mode 100644 auditbeat/module/file_integrity/kprobes/embed/4.18.0-193.1.2.el7.aarch64.btf create mode 100644 auditbeat/module/file_integrity/kprobes/embed/5.10.0-0.deb10.17-rt-arm64.btf create mode 100644 auditbeat/module/file_integrity/kprobes/embed/5.10.109-200.el7.aarch64.btf create mode 100644 auditbeat/module/file_integrity/kprobes/embed/5.8.0-1035-aws.btf create mode 100644 auditbeat/module/file_integrity/kprobes/verifier.go diff --git a/auditbeat/module/file_integrity/kprobes/embed/3.10.0-1062.1.1.el7.x86_64.btf b/auditbeat/module/file_integrity/kprobes/embed/3.10.0-1062.1.1.el7.x86_64.btf new file mode 100644 index 0000000000000000000000000000000000000000..5aa17730ece93a5cde48c54dcace003de95e27c2 GIT binary patch literal 1666 zcmbW1yKfUg5XQ&Gpd>gD9^w7Cg!dZ~f`o*SB`A=fL83%jF80|uY~LB**-|75R}>Wd z0W@j(8)$GzM@d5gEp#-L@SDB8*olNt80FjV+wsiq%-kw#KMHb4GW%K*QM@jQ@F&|u zN<(-890RKNfL%ZXH18-Fpgowl(u1PNS+wreu7H!k1B>7UsDRtxxN36m!c#!Vy$?-; zA^MTi6$*Kh`_WI)Sb zKB6xIwNGFYRKRDD_xc5W8C1aEX0~}Y`n@&&8^FqYGnO6Hb_4w#%z?b7pXlr07q|v~ z1HE&SBCf*DN_;A7)IWU_rT6N4njvlh{#$*s{Eihg-&2*DJx_lce>YnD5Nv)oOZWx^ zd+=7j=Phz7lz2wXfo+7xG1*McbRu1d{x{4Np^`RxD-qqr7`TTR<;OfRfSXc~4+mXZ9< z>Zc(&p=mp=TJA+j*ocCfS6%VDlJq)3*DSZR3+`3B%?_AMZyWTk^BltW0MB0mel;zw~GlA zsfVo~Yq%Q6%`lLJ%Vono$x7T!y$!A=12CyHnFiF;*}%s178h8XK{pCo+9A$tU3Y35 NHq-@a{dxq6e*h*h%1!_P literal 0 HcmV?d00001 diff --git a/auditbeat/module/file_integrity/kprobes/embed/4.14.101-91.76.amzn2.aarch64.btf b/auditbeat/module/file_integrity/kprobes/embed/4.14.101-91.76.amzn2.aarch64.btf new file mode 100644 index 0000000000000000000000000000000000000000..94fd094090c2350752147cdede753b9499cbc97a GIT binary patch literal 1497 zcmbW0yKfUg5XR?>feBDB7o12GF!HzGH{+Sv-SK*J%ad6d*=$op@zE3EqvlPp z_~0nOyavl5a77)F=K4O71@tMf4jur^qm8t{-|`-767|~a2{;XW;H;_j&jI^4pRwsn z^f~wCYxpc^fe@SlK6nG>RMR+vArWdTXbKF`Yfk%nL^kR;`Yl@B$q@^o>5FJR=LMi= zY=KRn^Lh`ksS_-p0Ils^KBF%KwJ%^A_~0wR>UnzFk3E`0_#0?}A@~lC06qKkOVLZ?3jU_n%h3ajTBnr@Za#=wqWvmUdfJTEyKn z3R`hnO0ksX<*rcGk8-n^WbK|5CX5EMqoN;_OA|;Ssqaphd5ok7K=!} zXAR1D*=9LzTQ;Usq*)nvR!u+1=_kuU(UUqTnpE_owv+=?j3`yMchIAUbzNxEfDJ<3 zj^h+%P)Z)HR8dhH+Y_0unobgQ3t1`3Tx|aKt=daLE`v^Cx)G&iZqm$dn3*z45($Gc zu#1Tc>BLDi+OC~ty*LtMs(TBpy(mwkM8{<~nq7NL0;k7Vph|}XZ>Q=-#(ll8=F#pq DmITi) literal 0 HcmV?d00001 diff --git a/auditbeat/module/file_integrity/kprobes/embed/4.18.0-193.1.2.el7.aarch64.btf b/auditbeat/module/file_integrity/kprobes/embed/4.18.0-193.1.2.el7.aarch64.btf new file mode 100644 index 0000000000000000000000000000000000000000..98d0968b69213a64f1b9cc1acda7ccf02d61cfc8 GIT binary patch literal 1626 zcmbW1xo;Ce6o;RU2@o8vaEa60_n8nH3Ku96N(BuCLZgj6b{5;a&d%CWgoJ`b0VNfp zKs4!6Qqe^ewEP9MG>DRhitmkgokL2KCx7$%y_wmaH!rJv{#}rxGKj^Ji1Mu!X+RIJR%jC1fslupgNXxZ5s6Xedr9FhAL=m6B#>;>K=V}zWxM% z95jmTIgjRRPx0sAIZ$8eEgNe1%b@WBw6+R#;UZL_4;R!^%uDnD=*wRtyI}?YF7s6i zd!NtaH|xPi_VAd$+Y{j}~V|)lQb`zb3Gw>H~z&|(vJE)2JWgI2)d-{aIe-jth zuN5W+dHHa*YlJf<@DG^Rd*PHR%TFoYZZUCJWLDTuVY%4-*NaxNaphoq_bi zI!V)yZWhZrV~~a&zbn(4R~=l})}|&U?HfG=iB#QOoTQa?6}982=}Q~V*5-ml>ZThc zRE5#oA%lt>wM$~>d)B4K*NaUSOrsq%o%ojxU!)iIGs83Tj!7Du)VAAF57NL7CUnvW z+jekTEsk5Em3fyY;*U2be>AVlnBK%)QH*KXB(iPowxM7@w&)c}i^d^6Z6j)o)27g$ HsY%lR;se~M literal 0 HcmV?d00001 diff --git a/auditbeat/module/file_integrity/kprobes/embed/5.10.0-0.deb10.17-rt-arm64.btf b/auditbeat/module/file_integrity/kprobes/embed/5.10.0-0.deb10.17-rt-arm64.btf new file mode 100644 index 0000000000000000000000000000000000000000..8189466782bbe3c3e1bf9dc258dac55509e88679 GIT binary patch literal 1334 zcmY*YJCD;q5FQ5$LP&Uu;|;mvbv%XObQBclmI8@?uyX88VkNN??>dm?bPb|`;#wM@ zL879gqChlMl$4Y-P|#9P@XdOB$&K>u_s#6=?#z1g`nN|XskEnih#2oYBK+8zM@uH3 zsC5du0dW2i-~_NFI0HToTn1`H{$cPPfa7fUfjSTX8vxAXGdT8$J)DJgq?{zL%AJUh z^QjGNiZAotbNJZDo%aIz91s9vKCfHUTt8a$dC@jB_Yy$A0`CX5px=Ve1Kh}a5YF=d zLVti>;W+Fgune?;Pr?EC3vdD8J->qY0=&;R5YDB~(BDM|2>htxJPG4_z6$;cTmgOo zmw-ROMc^N>52#~*Sana{5`6r8o?L;rhRH2pRq}8w@i$>ZO6xXw@f+TP&)@1M@BnML z{|)dBFy8_D58-nSTpOgj^XfeWh&@IS);#FQ4SZ~6PIt_@{a?X`E35=3@6ZknSLV{=Nwk*2s>8Y}UW zG>bLKs7B6FMG{k@dTulwNX%mlxm55-IC4A=ZJ&${E#-PrXk(S@5;C8u?lA0`ve2D! zOflBE>I}1JKt{!SN~WLXc6TRnl2bPs>axx#%La+2Y1gQp#!)s`X~tKiRB`VvM#Hy< e>eknkr2#5uLK~ohn__l}1D&UO$fqJyj{gC@OTMZA literal 0 HcmV?d00001 diff --git a/auditbeat/module/file_integrity/kprobes/embed/5.10.109-200.el7.aarch64.btf b/auditbeat/module/file_integrity/kprobes/embed/5.10.109-200.el7.aarch64.btf new file mode 100644 index 0000000000000000000000000000000000000000..6a03590781951669bfd0da0c97fec3afa6921e69 GIT binary patch literal 1334 zcmZ{jyKfUg5XQ&BgkT&VB0R#=yuzCzL}@_6Ee)bUqGEONt?iTTJLmRnVbKJM0vafx zi71dLsHkXYLP<%<-vB)w;x~7DRuE#8Z@+IQv$HdE$=e?lIVy!c-XWrRUlHL)QQZ&E zg2SK*yj81+JwS6dc8VN99|IfUB)}?~(*RchR=-~>?p2u3w;5o{RV!1ndMDvx({`9{>!k=cL^+zvji`~`kj|x{VvPJ-TFba+Ai+hfVc1K zI==4f8n{Kieuq2oRap0={#|^nd%vhx_Ch4L+U`;@&LQ+#kvFG3biLN^)Aw3yJ@I-G z?QN!BD(cW{X}Ye`{;PG>PkqgK^mDKE!Yi-$qF;Nh```5XJlfm#`ZfLAFXI?N~;!i#o9l8&RBzF>MzoS=64G zVUSuPkn!IvoqBsey^pm)g*P0e?b%Rs}f$K>aWI=wI zY%;Vm46?2iw<0sp%aIvHA!jE)-5QeLa|T&N_hfl)!^gL)%(ik!)#S66WV01`F8o&W#< literal 0 HcmV?d00001 diff --git a/auditbeat/module/file_integrity/kprobes/embed/5.8.0-1035-aws.btf b/auditbeat/module/file_integrity/kprobes/embed/5.8.0-1035-aws.btf new file mode 100644 index 0000000000000000000000000000000000000000..3a9f73b1e35d163ec0364480f01ef2ad7ef9242e GIT binary patch literal 1336 zcmZvbJ8u&~6os!B1HmNWC7$8o0O1iHMTml^u!Lw31!Y7tY`kN8<<0J9XKdjDDUm2B zXrMx(fEpS^NtKe{z+XT#G&B^PvomWa5?A`>oICHiUVr#mlT+d%-y=f4)Lgz#Waca^ z(>ZVeoB^}oG&lyhmSZQu0yqwCf+>;uY9O)%-vo?%-~woZ``|oifrsE6Jq~&V1ELLBHp3c-E=~{(!1~3w|E7z+b@r z&Aq54%vI!@U=|)x<9nNQF3R`QqJL%)x&`LJ&fn}h@;R)(24!&WZ7A<>3oKyl26Pe1 z-|;S#zx4wsV*J*}KBw^gPhCEJ1U_+oirzw_4Y!VMfNxn|`+nZ}t@jr>>Y~B#*++00=DnZ8T72^UN;Ts@ z_pjDYrpV4aH>M{80skXD+#H)JtCK|5L(`Wiv|-t(*hnW%v`WKuE$uAp$6Devi_ofd zCxbXb(k)xF28(gDp^dQwv%I!T?U*#Pac89Ru+XWMDwhaZF_Nenh}@A8cf)C1Y;xUR zSr$7~Mz<*`HObO$vutAQSDHa;;%=&=CVNaTD{OOD*M_l$lrPFd+Y>X$b)nXhtlbx* zB0ZE{vsnyfN4XYbLo0>e7-(ZvSt!_Iq&i91HSTDhI9}cEg@p_|Ms+n-+CrrnZ#=lO lBucUEqiV`ZA0>ngSWu`^hR?){SC-4PuZvVCycts0{sXMM!T|sP literal 0 HcmV?d00001 diff --git a/auditbeat/module/file_integrity/kprobes/verifier.go b/auditbeat/module/file_integrity/kprobes/verifier.go new file mode 100644 index 00000000000..cc659b6c25c --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/verifier.go @@ -0,0 +1,85 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//go:build linux + +package kprobes + +import ( + "bytes" + "embed" + "errors" + "io/fs" + "strings" + + tkbtf "github.com/elastic/tk-btf" +) + +//go:embed embed +var embedBTFFolder embed.FS + +func loadAllSpecs() ([]*tkbtf.Spec, error) { + var specs []*tkbtf.Spec + + spec, err := tkbtf.NewSpecFromKernel() + if err != nil { + if !errors.Is(err, tkbtf.ErrSpecKernelNotSupported) { + return nil, err + } + } else { + specs = append(specs, spec) + } + + embeddedSpecs, err := loadEmbeddedSpecs() + if err != nil { + return nil, err + } + specs = append(specs, embeddedSpecs...) + return specs, nil +} + +func loadEmbeddedSpecs() ([]*tkbtf.Spec, error) { + var specs []*tkbtf.Spec + err := fs.WalkDir(embedBTFFolder, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if !strings.HasSuffix(path, ".btf") { + return nil + } + + embedFileBytes, err := embedBTFFolder.ReadFile(path) + if err != nil { + return err + } + + embedSpec, err := tkbtf.NewSpecFromReader(bytes.NewReader(embedFileBytes), nil) + if err != nil { + return err + } + + specs = append(specs, embedSpec) + return nil + }) + + if err != nil { + return nil, err + } + + return specs, nil +} From e4616c12ef483a8604bd9da154969215df25343c Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 24 Jan 2024 09:32:01 +0200 Subject: [PATCH 06/39] feat: add fsnotify, fsnotify_nameremove, fsnotify_parent and vfs_geattr tk-btf probe builders in probe manager --- .../module/file_integrity/kprobes/errors.go | 5 +- .../module/file_integrity/kprobes/probes.go | 33 +++ .../file_integrity/kprobes/probes_fsnotify.go | 194 ++++++++++++++++++ .../kprobes/probes_fsnotify_nameremove.go | 89 ++++++++ .../probes_fsnotify_nameremove_test.go | 106 ++++++++++ .../kprobes/probes_fsnotify_parent.go | 98 +++++++++ .../kprobes/probes_fsnotify_parent_test.go | 140 +++++++++++++ .../kprobes/probes_fsnotify_test.go | 100 +++++++++ .../kprobes/probes_vfs_getattr.go | 94 +++++++++ .../kprobes/probes_vfs_getattr_test.go | 147 +++++++++++++ 10 files changed, 1005 insertions(+), 1 deletion(-) create mode 100644 auditbeat/module/file_integrity/kprobes/probes_fsnotify.go create mode 100644 auditbeat/module/file_integrity/kprobes/probes_fsnotify_nameremove.go create mode 100644 auditbeat/module/file_integrity/kprobes/probes_fsnotify_nameremove_test.go create mode 100644 auditbeat/module/file_integrity/kprobes/probes_fsnotify_parent.go create mode 100644 auditbeat/module/file_integrity/kprobes/probes_fsnotify_parent_test.go create mode 100644 auditbeat/module/file_integrity/kprobes/probes_fsnotify_test.go create mode 100644 auditbeat/module/file_integrity/kprobes/probes_vfs_getattr.go create mode 100644 auditbeat/module/file_integrity/kprobes/probes_vfs_getattr_test.go diff --git a/auditbeat/module/file_integrity/kprobes/errors.go b/auditbeat/module/file_integrity/kprobes/errors.go index e42192bfe4c..fccb2149214 100644 --- a/auditbeat/module/file_integrity/kprobes/errors.go +++ b/auditbeat/module/file_integrity/kprobes/errors.go @@ -20,5 +20,8 @@ package kprobes import "errors" var ( - ErrSymbolNotFound = errors.New("symbol not found") + ErrVerifyOverlappingEvents = errors.New("probe overlapping events") + ErrVerifyMissingEvents = errors.New("probe missing events") + ErrVerifyUnexpectedEvent = errors.New("received an event that was not expected") + ErrSymbolNotFound = errors.New("symbol not found") ) diff --git a/auditbeat/module/file_integrity/kprobes/probes.go b/auditbeat/module/file_integrity/kprobes/probes.go index 126dae2c9dd..431d9fe2e3a 100644 --- a/auditbeat/module/file_integrity/kprobes/probes.go +++ b/auditbeat/module/file_integrity/kprobes/probes.go @@ -21,6 +21,23 @@ import ( "github.com/elastic/beats/v7/auditbeat/tracing" tkbtf "github.com/elastic/tk-btf" + + "golang.org/x/sys/unix" +) + +const ( + fsEventModify = uint32(unix.IN_MODIFY) + fsEventAttrib = uint32(unix.IN_ATTRIB) + fsEventMovedFrom = uint32(unix.IN_MOVED_FROM) + fsEventMovedTo = uint32(unix.IN_MOVED_TO) + fsEventCreate = uint32(unix.IN_CREATE) + fsEventDelete = uint32(unix.IN_DELETE) + fsEventIsDir = uint32(unix.IN_ISDIR) +) + +const ( + devMajor = uint32(0xFFF00000) + devMinor = uint32(0x3FF) ) type probeWithAllocFunc struct { @@ -49,6 +66,22 @@ func newProbeManager(e executor) (*probeManager, error) { getSymbolInfoRuntime: getSymbolInfoRuntime, } + if err := loadFsNotifySymbol(fs); err != nil { + return nil, err + } + + if err := loadFsNotifyParentSymbol(fs); err != nil { + return nil, err + } + + if err := loadFsNotifyNameRemoveSymbol(fs); err != nil { + return nil, err + } + + if err := loadVFSGetAttrSymbol(fs, e); err != nil { + return nil, err + } + return fs, nil } diff --git a/auditbeat/module/file_integrity/kprobes/probes_fsnotify.go b/auditbeat/module/file_integrity/kprobes/probes_fsnotify.go new file mode 100644 index 00000000000..5badf59f4eb --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/probes_fsnotify.go @@ -0,0 +1,194 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "errors" + "fmt" + + tkbtf "github.com/elastic/tk-btf" +) + +type fsNotifySymbol struct { + symbolName string + inodeProbeFilter string + dentryProbeFilter string + pathProbeFilter string + lastOnErr error + seenSpecs map[*tkbtf.Spec]struct{} +} + +func loadFsNotifySymbol(s *probeManager) error { + + symbolInfo, err := s.getSymbolInfoRuntime("fsnotify") + if err != nil { + return err + } + + if symbolInfo.isOptimised { + return fmt.Errorf("symbol %s is optimised", symbolInfo.symbolName) + } + + s.buildChecks = append(s.buildChecks, func(spec *tkbtf.Spec) bool { + return spec.ContainsSymbol(symbolInfo.symbolName) + }) + + // default filters for all three fsnotify probes enable mask_create, mask_delete, mask_attrib, mask_modify, + // mask_moved_to, and mask_moved_from events. + s.symbols = append(s.symbols, &fsNotifySymbol{ + symbolName: symbolInfo.symbolName, + }) + + return nil +} + +func (f *fsNotifySymbol) buildProbes(spec *tkbtf.Spec) ([]*probeWithAllocFunc, error) { + allocFunc := allocProbeEvent + + _, seen := f.seenSpecs[spec] + if !seen { + + if f.seenSpecs == nil { + f.seenSpecs = make(map[*tkbtf.Spec]struct{}) + } + + f.lastOnErr = nil + // reset probe filters for each new spec + // this probes shouldn't cause any ErrVerifyOverlappingEvents or ErrVerifyMissingEvents + // for linux kernel versions linux 5.17+, thus we start from here. To see how we handle all + // linux kernels filter variation check OnErr() method. + f.seenSpecs[spec] = struct{}{} + f.pathProbeFilter = "(mc==1 || md==1 || ma==1 || mm==1 || mmt==1 || mmf==1) && dt==1" + f.inodeProbeFilter = "(mc==1 || md==1 || ma==1 || mm==1 || mmt==1 || mmf==1) && dt==2 && nptr!=0" + f.dentryProbeFilter = "(mc==1 || md==1 || ma==1 || mm==1 || mmt==1 || mmf==1) && dt==3" + } + + pathProbe := tkbtf.NewKProbe().SetRef("fsnotify_path").AddFetchArgs( + tkbtf.NewFetchArg("pi", "u64").FuncParamWithCustomType("data", tkbtf.WrapPointer, "path", "dentry", "d_parent", "d_inode", "i_ino"), + tkbtf.NewFetchArg("mc", tkbtf.BitFieldTypeMask(fsEventCreate)).FuncParamWithName("mask"), + tkbtf.NewFetchArg("md", tkbtf.BitFieldTypeMask(fsEventDelete)).FuncParamWithName("mask"), + tkbtf.NewFetchArg("ma", tkbtf.BitFieldTypeMask(fsEventAttrib)).FuncParamWithName("mask"), + tkbtf.NewFetchArg("mm", tkbtf.BitFieldTypeMask(fsEventModify)).FuncParamWithName("mask"), + tkbtf.NewFetchArg("mid", tkbtf.BitFieldTypeMask(fsEventIsDir)).FuncParamWithName("mask"), + tkbtf.NewFetchArg("mmt", tkbtf.BitFieldTypeMask(fsEventMovedTo)).FuncParamWithName("mask"), + tkbtf.NewFetchArg("mmf", tkbtf.BitFieldTypeMask(fsEventMovedFrom)).FuncParamWithName("mask"), + tkbtf.NewFetchArg("fi", "u64").FuncParamWithCustomType("data", tkbtf.WrapPointer, "path", "dentry", "d_inode", "i_ino"), + tkbtf.NewFetchArg("dt", "s32").FuncParamWithName("data_type").FuncParamWithName("data_is"), + tkbtf.NewFetchArg("fdmj", tkbtf.BitFieldTypeMask(devMajor)).FuncParamWithCustomType("data", tkbtf.WrapPointer, "path", "dentry", "d_inode", "i_sb", "s_dev"), + tkbtf.NewFetchArg("fdmn", tkbtf.BitFieldTypeMask(devMinor)).FuncParamWithCustomType("data", tkbtf.WrapPointer, "path", "dentry", "d_inode", "i_sb", "s_dev"), + tkbtf.NewFetchArg("pdmj", tkbtf.BitFieldTypeMask(devMajor)).FuncParamWithCustomType("data", tkbtf.WrapPointer, "path", "dentry", "d_parent", "d_inode", "i_sb", "s_dev"), + tkbtf.NewFetchArg("pdmn", tkbtf.BitFieldTypeMask(devMinor)).FuncParamWithCustomType("data", tkbtf.WrapPointer, "path", "dentry", "d_parent", "d_inode", "i_sb", "s_dev"), + tkbtf.NewFetchArg("fn", "string").FuncParamWithCustomType("data", tkbtf.WrapPointer, "path", "dentry", "d_name", "name"), + ).SetFilter(f.pathProbeFilter) + + inodeProbe := tkbtf.NewKProbe().SetRef("fsnotify_inode").AddFetchArgs( + tkbtf.NewFetchArg("pi", "u64").FuncParamWithName("dir", "i_ino").FuncParamWithName("to_tell", "i_ino"), + tkbtf.NewFetchArg("mc", tkbtf.BitFieldTypeMask(fsEventCreate)).FuncParamWithName("mask"), + tkbtf.NewFetchArg("md", tkbtf.BitFieldTypeMask(fsEventDelete)).FuncParamWithName("mask"), + tkbtf.NewFetchArg("ma", tkbtf.BitFieldTypeMask(fsEventAttrib)).FuncParamWithName("mask"), + tkbtf.NewFetchArg("mm", tkbtf.BitFieldTypeMask(fsEventModify)).FuncParamWithName("mask"), + tkbtf.NewFetchArg("mid", tkbtf.BitFieldTypeMask(fsEventIsDir)).FuncParamWithName("mask"), + tkbtf.NewFetchArg("mmt", tkbtf.BitFieldTypeMask(fsEventMovedTo)).FuncParamWithName("mask"), + tkbtf.NewFetchArg("mmf", tkbtf.BitFieldTypeMask(fsEventMovedFrom)).FuncParamWithName("mask"), + tkbtf.NewFetchArg("nptr", "u64").FuncParamWithName("file_name"), + tkbtf.NewFetchArg("fi", "u64").FuncParamWithCustomType("data", tkbtf.WrapPointer, "inode", "i_ino"), + tkbtf.NewFetchArg("dt", "s32").FuncParamWithName("data_type").FuncParamWithName("data_is"), + tkbtf.NewFetchArg("fdmj", tkbtf.BitFieldTypeMask(devMajor)).FuncParamWithCustomType("data", tkbtf.WrapPointer, "inode", "i_sb", "s_dev"), + tkbtf.NewFetchArg("fdmn", tkbtf.BitFieldTypeMask(devMinor)).FuncParamWithCustomType("data", tkbtf.WrapPointer, "inode", "i_sb", "s_dev"), + tkbtf.NewFetchArg("pdmj", tkbtf.BitFieldTypeMask(devMajor)).FuncParamWithName("dir", "i_sb", "s_dev").FuncParamWithName("to_tell", "i_sb", "s_dev"), + tkbtf.NewFetchArg("pdmn", tkbtf.BitFieldTypeMask(devMinor)).FuncParamWithName("dir", "i_sb", "s_dev").FuncParamWithName("to_tell", "i_sb", "s_dev"), + tkbtf.NewFetchArg("fn", "string").FuncParamWithName("file_name", "name").FuncParamWithName("file_name"), + ).SetFilter(f.inodeProbeFilter) + + dentryProbe := tkbtf.NewKProbe().SetRef("fsnotify_dentry").AddFetchArgs( + tkbtf.NewFetchArg("mc", tkbtf.BitFieldTypeMask(fsEventCreate)).FuncParamWithName("mask"), + tkbtf.NewFetchArg("md", tkbtf.BitFieldTypeMask(fsEventDelete)).FuncParamWithName("mask"), + tkbtf.NewFetchArg("ma", tkbtf.BitFieldTypeMask(fsEventAttrib)).FuncParamWithName("mask"), + tkbtf.NewFetchArg("mm", tkbtf.BitFieldTypeMask(fsEventModify)).FuncParamWithName("mask"), + tkbtf.NewFetchArg("mid", tkbtf.BitFieldTypeMask(fsEventIsDir)).FuncParamWithName("mask"), + tkbtf.NewFetchArg("mmt", tkbtf.BitFieldTypeMask(fsEventMovedTo)).FuncParamWithName("mask"), + tkbtf.NewFetchArg("mmf", tkbtf.BitFieldTypeMask(fsEventMovedFrom)).FuncParamWithName("mask"), + tkbtf.NewFetchArg("fi", "u64").FuncParamWithCustomType("data", tkbtf.WrapPointer, "dentry", "d_inode", "i_ino"), + tkbtf.NewFetchArg("dt", "s32").FuncParamWithName("data_type").FuncParamWithName("data_is"), + tkbtf.NewFetchArg("fdmj", tkbtf.BitFieldTypeMask(devMajor)).FuncParamWithCustomType("data", tkbtf.WrapPointer, "dentry", "d_inode", "i_sb", "s_dev"), + tkbtf.NewFetchArg("fdmn", tkbtf.BitFieldTypeMask(devMinor)).FuncParamWithCustomType("data", tkbtf.WrapPointer, "dentry", "d_inode", "i_sb", "s_dev"), + tkbtf.NewFetchArg("pi", "u64").FuncParamWithCustomType("data", tkbtf.WrapPointer, "dentry", "d_parent", "d_inode", "i_ino"), + tkbtf.NewFetchArg("pdmj", tkbtf.BitFieldTypeMask(devMajor)).FuncParamWithCustomType("data", tkbtf.WrapPointer, "dentry", "d_parent", "d_inode", "i_sb", "s_dev"), + tkbtf.NewFetchArg("pdmn", tkbtf.BitFieldTypeMask(devMinor)).FuncParamWithCustomType("data", tkbtf.WrapPointer, "dentry", "d_parent", "d_inode", "i_sb", "s_dev"), + tkbtf.NewFetchArg("fn", "string").FuncParamWithCustomType("data", tkbtf.WrapPointer, "dentry", "d_name", "name"), + ).SetFilter(f.dentryProbeFilter) + + btfSymbol := tkbtf.NewSymbol(f.symbolName).AddProbes( + inodeProbe, + dentryProbe, + pathProbe, + ) + + if err := spec.BuildSymbol(btfSymbol); err != nil { + return nil, err + } + + return []*probeWithAllocFunc{ + { + probe: inodeProbe, + allocateFn: allocFunc, + }, + { + probe: dentryProbe, + allocateFn: allocFunc, + }, + { + probe: pathProbe, + allocateFn: allocFunc, + }, + }, nil +} + +func (f *fsNotifySymbol) onErr(err error) bool { + if f.lastOnErr != nil && errors.Is(err, f.lastOnErr) { + return false + } + + f.lastOnErr = err + + switch { + case errors.Is(err, ErrVerifyOverlappingEvents): + + // on ErrVerifyOverlappingEvents for linux kernel versions < 5.7 the __fsnotify_parent + // probe is capturing and sending the modify events as well, thus disable them for + // fsnotify and return true to signal a retry. + f.inodeProbeFilter = "(mc==1 || md==1 || ma==1 || mmt==1 || mmf==1) && dt==2 && nptr!=0" + f.dentryProbeFilter = "(mc==1 || md==1 || ma==1 || mmt==1 || mmf==1) && dt==3" + f.pathProbeFilter = "(mc==1 || md==1 || ma==1 || mmt==1 || mmf==1) && dt==1" + + return true + case errors.Is(err, ErrVerifyMissingEvents): + + // on ErrVerifyMissingEvents for linux kernel versions 5.10 - 5.16 the __fsnotify_parent + // probe is not capturing and sending the modify attributes events for directories, thus + // we adjust the filters to allow them flowing through fsnotify and return true to signal + // a retry. + f.pathProbeFilter = "(mc==1 || md==1 || ma==1 || mm==1 || mmt==1 || mmf==1) && dt==1" + f.inodeProbeFilter = "(mc==1 || md==1 || ma==1 || mm==1 || mmt==1 || mmf==1) && dt==2 && (nptr!=0 || (mid==1 && ma==1))" + f.dentryProbeFilter = "(mc==1 || md==1 || ma==1 || mm==1 || mmt==1 || mmf==1) && dt==3" + + return true + default: + return false + } +} diff --git a/auditbeat/module/file_integrity/kprobes/probes_fsnotify_nameremove.go b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_nameremove.go new file mode 100644 index 00000000000..9ecad77d8f7 --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_nameremove.go @@ -0,0 +1,89 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "errors" + "fmt" + + tkbtf "github.com/elastic/tk-btf" +) + +type fsNotifyNameRemoveSymbol struct { + symbolName string +} + +func loadFsNotifyNameRemoveSymbol(s *probeManager) error { + + symbolInfo, err := s.getSymbolInfoRuntime("fsnotify_nameremove") + if err != nil { + if errors.Is(err, ErrSymbolNotFound) { + s.buildChecks = append(s.buildChecks, func(spec *tkbtf.Spec) bool { + return !spec.ContainsSymbol(symbolInfo.symbolName) + }) + return nil + } + return err + } + + if symbolInfo.isOptimised { + return fmt.Errorf("symbol %s is optimised", symbolInfo.symbolName) + } + + s.buildChecks = append(s.buildChecks, func(spec *tkbtf.Spec) bool { + return spec.ContainsSymbol(symbolInfo.symbolName) + }) + + s.symbols = append(s.symbols, &fsNotifyNameRemoveSymbol{ + symbolName: symbolInfo.symbolName, + }) + + return nil +} + +func (f *fsNotifyNameRemoveSymbol) buildProbes(spec *tkbtf.Spec) ([]*probeWithAllocFunc, error) { + allocFunc := allocDeleteProbeEvent + + probe := tkbtf.NewKProbe().AddFetchArgs( + tkbtf.NewFetchArg("mid", "u32").FuncParamWithName("isdir"), + tkbtf.NewFetchArg("fi", "u64").FuncParamWithName("dentry", "d_inode", "i_ino"), + tkbtf.NewFetchArg("fdmj", tkbtf.BitFieldTypeMask(devMajor)).FuncParamWithName("dentry", "d_inode", "i_sb", "s_dev"), + tkbtf.NewFetchArg("fdmn", tkbtf.BitFieldTypeMask(devMinor)).FuncParamWithName("dentry", "d_inode", "i_sb", "s_dev"), + tkbtf.NewFetchArg("pi", "u64").FuncParamWithName("dentry", "d_parent", "d_inode", "i_ino"), + tkbtf.NewFetchArg("pdmj", tkbtf.BitFieldTypeMask(devMajor)).FuncParamWithName("dentry", "d_parent", "d_inode", "i_sb", "s_dev"), + tkbtf.NewFetchArg("pdmn", tkbtf.BitFieldTypeMask(devMinor)).FuncParamWithName("dentry", "d_parent", "d_inode", "i_sb", "s_dev"), + tkbtf.NewFetchArg("fn", "string").FuncParamWithName("dentry", "d_name", "name"), + ) + + btfSymbol := tkbtf.NewSymbol(f.symbolName).AddProbes(probe) + + if err := spec.BuildSymbol(btfSymbol); err != nil { + return nil, err + } + + return []*probeWithAllocFunc{ + { + probe: probe, + allocateFn: allocFunc, + }, + }, nil +} + +func (f *fsNotifyNameRemoveSymbol) onErr(_ error) bool { + return false +} diff --git a/auditbeat/module/file_integrity/kprobes/probes_fsnotify_nameremove_test.go b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_nameremove_test.go new file mode 100644 index 00000000000..aeb047fe6b0 --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_nameremove_test.go @@ -0,0 +1,106 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_fsNotifyNameRemoveSymbol_buildProbes(t *testing.T) { + specs, err := loadEmbeddedSpecs() + require.NoError(t, err) + require.NotEmpty(t, specs) + + s := &fsNotifyNameRemoveSymbol{} + + for _, spec := range specs { + switch { + case spec.ContainsSymbol("fsnotify_nameremove"): + s.symbolName = "fsnotify_nameremove" + default: + continue + } + + _, err := s.buildProbes(spec) + require.NoError(t, err) + } +} + +func Test_fsNotifyNameRemoveSymbol_load(t *testing.T) { + prbMgr := &probeManager{ + symbols: nil, + buildChecks: nil, + getSymbolInfoRuntime: nil, + } + + prbMgr.getSymbolInfoRuntime = func(symbolName string) (runtimeSymbolInfo, error) { + return runtimeSymbolInfo{}, ErrSymbolNotFound + } + require.NoError(t, loadFsNotifyNameRemoveSymbol(prbMgr)) + require.Equal(t, len(prbMgr.symbols), 0) + require.Equal(t, len(prbMgr.buildChecks), 1) + + prbMgr = &probeManager{ + symbols: nil, + buildChecks: nil, + getSymbolInfoRuntime: nil, + } + prbMgr.getSymbolInfoRuntime = func(symbolName string) (runtimeSymbolInfo, error) { + if symbolName != "fsnotify_nameremove" { + return runtimeSymbolInfo{}, ErrSymbolNotFound + } + + return runtimeSymbolInfo{ + symbolName: "fsnotify_nameremove", + isOptimised: true, + optimisedSymbolName: "fsnotify_nameremove.isra.0", + }, nil + } + require.Error(t, loadFsNotifyNameRemoveSymbol(prbMgr)) + + unknownErr := errors.New("unknown error") + prbMgr.getSymbolInfoRuntime = func(symbolName string) (runtimeSymbolInfo, error) { + return runtimeSymbolInfo{}, unknownErr + } + require.Error(t, loadFsNotifyNameRemoveSymbol(prbMgr)) + + prbMgr.getSymbolInfoRuntime = func(symbolName string) (runtimeSymbolInfo, error) { + return runtimeSymbolInfo{ + symbolName: "fsnotify_nameremove", + isOptimised: false, + optimisedSymbolName: "", + }, nil + } + + require.NoError(t, loadFsNotifyNameRemoveSymbol(prbMgr)) + require.Equal(t, len(prbMgr.symbols), 1) + require.Equal(t, len(prbMgr.buildChecks), 1) +} + +func Test_fsNotifyNameRemoveSymbol_onErr(t *testing.T) { + s := &fsNotifyNameRemoveSymbol{} + + testErr := fmt.Errorf("test: %w", ErrVerifyOverlappingEvents) + repeat := s.onErr(testErr) + require.False(t, repeat) + +} diff --git a/auditbeat/module/file_integrity/kprobes/probes_fsnotify_parent.go b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_parent.go new file mode 100644 index 00000000000..f0936ed81af --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_parent.go @@ -0,0 +1,98 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "errors" + "fmt" + + tkbtf "github.com/elastic/tk-btf" +) + +type fsNotifyParentSymbol struct { + symbolName string + filter string +} + +func loadFsNotifyParentSymbol(s *probeManager) error { + + symbolInfo, err := s.getSymbolInfoRuntime("__fsnotify_parent") + if err != nil { + if !errors.Is(err, ErrSymbolNotFound) { + return err + } + + symbolInfo, err = s.getSymbolInfoRuntime("fsnotify_parent") + if err != nil { + return err + } + } + + if symbolInfo.isOptimised { + return fmt.Errorf("symbol %s is optimised", symbolInfo.symbolName) + } + + s.buildChecks = append(s.buildChecks, func(spec *tkbtf.Spec) bool { + return spec.ContainsSymbol(symbolInfo.symbolName) + }) + + s.symbols = append(s.symbols, &fsNotifyParentSymbol{ + symbolName: symbolInfo.symbolName, + filter: "(mc==1 || md==1 || ma==1 || mm==1)", + }) + + return nil +} + +func (f *fsNotifyParentSymbol) buildProbes(spec *tkbtf.Spec) ([]*probeWithAllocFunc, error) { + allocFunc := allocProbeEvent + + probe := tkbtf.NewKProbe().AddFetchArgs( + tkbtf.NewFetchArg("mc", tkbtf.BitFieldTypeMask(fsEventCreate)).FuncParamWithName("mask"), + tkbtf.NewFetchArg("md", tkbtf.BitFieldTypeMask(fsEventDelete)).FuncParamWithName("mask"), + tkbtf.NewFetchArg("ma", tkbtf.BitFieldTypeMask(fsEventAttrib)).FuncParamWithName("mask"), + tkbtf.NewFetchArg("mm", tkbtf.BitFieldTypeMask(fsEventModify)).FuncParamWithName("mask"), + tkbtf.NewFetchArg("mid", tkbtf.BitFieldTypeMask(fsEventIsDir)).FuncParamWithName("mask"), + tkbtf.NewFetchArg("mmt", tkbtf.BitFieldTypeMask(fsEventMovedTo)).FuncParamWithName("mask"), + tkbtf.NewFetchArg("mmf", tkbtf.BitFieldTypeMask(fsEventMovedFrom)).FuncParamWithName("mask"), + tkbtf.NewFetchArg("fi", "u64").FuncParamWithName("dentry", "d_inode", "i_ino"), + tkbtf.NewFetchArg("fdmj", tkbtf.BitFieldTypeMask(devMajor)).FuncParamWithName("dentry", "d_inode", "i_sb", "s_dev"), + tkbtf.NewFetchArg("fdmn", tkbtf.BitFieldTypeMask(devMinor)).FuncParamWithName("dentry", "d_inode", "i_sb", "s_dev"), + tkbtf.NewFetchArg("pi", "u64").FuncParamWithName("dentry", "d_parent", "d_inode", "i_ino"), + tkbtf.NewFetchArg("pdmj", tkbtf.BitFieldTypeMask(devMajor)).FuncParamWithName("dentry", "d_parent", "d_inode", "i_sb", "s_dev"), + tkbtf.NewFetchArg("pdmn", tkbtf.BitFieldTypeMask(devMinor)).FuncParamWithName("dentry", "d_parent", "d_inode", "i_sb", "s_dev"), + tkbtf.NewFetchArg("fn", "string").FuncParamWithName("dentry", "d_name", "name"), + ).SetFilter(f.filter) + + btfSymbol := tkbtf.NewSymbol(f.symbolName).AddProbes(probe) + + if err := spec.BuildSymbol(btfSymbol); err != nil { + return nil, err + } + + return []*probeWithAllocFunc{ + { + probe: probe, + allocateFn: allocFunc, + }, + }, nil +} + +func (f *fsNotifyParentSymbol) onErr(_ error) bool { + return false +} diff --git a/auditbeat/module/file_integrity/kprobes/probes_fsnotify_parent_test.go b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_parent_test.go new file mode 100644 index 00000000000..99c0c32957d --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_parent_test.go @@ -0,0 +1,140 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_fsNotifyParentSymbol_buildProbes(t *testing.T) { + specs, err := loadEmbeddedSpecs() + require.NoError(t, err) + require.NotEmpty(t, specs) + + s := &fsNotifyParentSymbol{} + + for _, spec := range specs { + switch { + case spec.ContainsSymbol("__fsnotify_parent"): + s.symbolName = "__fsnotify_parent" + case spec.ContainsSymbol("fsnotify_parent"): + s.symbolName = "fsnotify_parent" + default: + t.FailNow() + } + + _, err := s.buildProbes(spec) + require.NoError(t, err) + } +} + +func Test_fsNotifyParentSymbol_load(t *testing.T) { + prbMgr := &probeManager{ + symbols: nil, + buildChecks: nil, + getSymbolInfoRuntime: nil, + } + + prbMgr.getSymbolInfoRuntime = func(symbolName string) (runtimeSymbolInfo, error) { + return runtimeSymbolInfo{}, ErrSymbolNotFound + } + require.ErrorIs(t, loadFsNotifyParentSymbol(prbMgr), ErrSymbolNotFound) + + prbMgr = &probeManager{ + symbols: nil, + buildChecks: nil, + getSymbolInfoRuntime: nil, + } + prbMgr.getSymbolInfoRuntime = func(symbolName string) (runtimeSymbolInfo, error) { + if symbolName == "fsnotify_parent" { + return runtimeSymbolInfo{ + symbolName: "fsnotify_parent", + isOptimised: true, + optimisedSymbolName: "fsnotify_parent.isra.0", + }, nil + } + return runtimeSymbolInfo{}, ErrSymbolNotFound + } + require.Error(t, loadFsNotifyParentSymbol(prbMgr)) + + prbMgr = &probeManager{ + symbols: nil, + buildChecks: nil, + getSymbolInfoRuntime: nil, + } + prbMgr.getSymbolInfoRuntime = func(symbolName string) (runtimeSymbolInfo, error) { + if symbolName == "fsnotify_parent" { + return runtimeSymbolInfo{ + symbolName: "fsnotify_parent", + isOptimised: false, + optimisedSymbolName: "", + }, nil + } + return runtimeSymbolInfo{}, ErrSymbolNotFound + } + require.NoError(t, loadFsNotifyParentSymbol(prbMgr)) + require.NotEmpty(t, prbMgr.symbols) + require.NotEmpty(t, prbMgr.buildChecks) + require.IsType(t, &fsNotifyParentSymbol{}, prbMgr.symbols[0]) + require.Equal(t, prbMgr.symbols[0].(*fsNotifyParentSymbol).symbolName, "fsnotify_parent") + + prbMgr = &probeManager{ + symbols: nil, + buildChecks: nil, + getSymbolInfoRuntime: nil, + } + prbMgr.getSymbolInfoRuntime = func(symbolName string) (runtimeSymbolInfo, error) { + if symbolName == "__fsnotify_parent" { + return runtimeSymbolInfo{ + symbolName: "__fsnotify_parent", + isOptimised: false, + optimisedSymbolName: "", + }, nil + } + return runtimeSymbolInfo{}, ErrSymbolNotFound + } + require.NoError(t, loadFsNotifyParentSymbol(prbMgr)) + require.NotEmpty(t, prbMgr.symbols) + require.NotEmpty(t, prbMgr.buildChecks) + require.IsType(t, &fsNotifyParentSymbol{}, prbMgr.symbols[0]) + require.Equal(t, prbMgr.symbols[0].(*fsNotifyParentSymbol).symbolName, "__fsnotify_parent") + + prbMgr = &probeManager{ + symbols: nil, + buildChecks: nil, + getSymbolInfoRuntime: nil, + } + unknownErr := errors.New("unknown error") + prbMgr.getSymbolInfoRuntime = func(symbolName string) (runtimeSymbolInfo, error) { + return runtimeSymbolInfo{}, unknownErr + } + require.Error(t, loadFsNotifyParentSymbol(prbMgr)) +} + +func Test_fsNotifyParentSymbol_onErr(t *testing.T) { + s := &fsNotifyParentSymbol{} + + testErr := fmt.Errorf("test: %w", ErrVerifyOverlappingEvents) + repeat := s.onErr(testErr) + require.False(t, repeat) + +} diff --git a/auditbeat/module/file_integrity/kprobes/probes_fsnotify_test.go b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_test.go new file mode 100644 index 00000000000..18378b0b925 --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_test.go @@ -0,0 +1,100 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_fsNotifySymbol_buildProbes(t *testing.T) { + specs, err := loadEmbeddedSpecs() + require.NoError(t, err) + require.NotEmpty(t, specs) + + s := &fsNotifySymbol{ + symbolName: "fsnotify", + lastOnErr: nil, + } + + for _, spec := range specs { + + if !spec.ContainsSymbol("fsnotify") { + t.FailNow() + } + + _, err := s.buildProbes(spec) + require.NoError(t, err) + } +} + +func Test_fsNotifySymbol_load(t *testing.T) { + prbMgr := &probeManager{ + symbols: nil, + buildChecks: nil, + getSymbolInfoRuntime: nil, + } + + prbMgr.getSymbolInfoRuntime = func(symbolName string) (runtimeSymbolInfo, error) { + return runtimeSymbolInfo{}, ErrSymbolNotFound + } + require.ErrorIs(t, loadFsNotifySymbol(prbMgr), ErrSymbolNotFound) + + prbMgr.getSymbolInfoRuntime = func(symbolName string) (runtimeSymbolInfo, error) { + if symbolName != "fsnotify" { + return runtimeSymbolInfo{}, ErrSymbolNotFound + } + + return runtimeSymbolInfo{ + symbolName: "fsnotify", + isOptimised: true, + optimisedSymbolName: "fsnotify.isra.0", + }, nil + } + + require.Error(t, loadFsNotifySymbol(prbMgr)) + + prbMgr.getSymbolInfoRuntime = func(symbolName string) (runtimeSymbolInfo, error) { + return runtimeSymbolInfo{ + symbolName: "fsnotify", + isOptimised: false, + optimisedSymbolName: "", + }, nil + } + + require.NoError(t, loadFsNotifySymbol(prbMgr)) + require.Equal(t, len(prbMgr.symbols), 1) + require.Equal(t, len(prbMgr.buildChecks), 1) +} + +func Test_fsNotifySymbol_onErr(t *testing.T) { + s := &fsNotifySymbol{ + symbolName: "fsnotify", + lastOnErr: nil, + } + + require.True(t, s.onErr(ErrVerifyOverlappingEvents)) + + require.True(t, s.onErr(ErrVerifyMissingEvents)) + + require.False(t, s.onErr(ErrVerifyMissingEvents)) + + require.False(t, s.onErr(ErrVerifyUnexpectedEvent)) + +} diff --git a/auditbeat/module/file_integrity/kprobes/probes_vfs_getattr.go b/auditbeat/module/file_integrity/kprobes/probes_vfs_getattr.go new file mode 100644 index 00000000000..d5a1a8a7d75 --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/probes_vfs_getattr.go @@ -0,0 +1,94 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "errors" + "fmt" + + tkbtf "github.com/elastic/tk-btf" +) + +type vfsGetAttrSymbol struct { + symbolName string + filter string +} + +func loadVFSGetAttrSymbol(s *probeManager, e executor) error { + + // get the vfs_getattr_nosec symbol information + symbolInfo, err := s.getSymbolInfoRuntime("vfs_getattr_nosec") + if err != nil { + if !errors.Is(err, ErrSymbolNotFound) { + return err + } + + // for older kernel versions use the vfs_getattr symbol + symbolInfo, err = s.getSymbolInfoRuntime("vfs_getattr") + if err != nil { + return err + } + } + + // we do not support optimised symbols + if symbolInfo.isOptimised { + return fmt.Errorf("symbol %s is optimised", symbolInfo.symbolName) + } + + s.buildChecks = append(s.buildChecks, func(spec *tkbtf.Spec) bool { + return spec.ContainsSymbol(symbolInfo.symbolName) + }) + + s.symbols = append(s.symbols, &vfsGetAttrSymbol{ + symbolName: symbolInfo.symbolName, + filter: fmt.Sprintf("common_pid==%d", e.GetTID()), + }) + + return nil +} + +func (f *vfsGetAttrSymbol) buildProbes(spec *tkbtf.Spec) ([]*probeWithAllocFunc, error) { + allocFunc := allocMonitorProbeEvent + + probe := tkbtf.NewKProbe().AddFetchArgs( + tkbtf.NewFetchArg("pi", "u64").FuncParamWithName("path", "dentry", "d_parent", "d_inode", "i_ino"), + tkbtf.NewFetchArg("fi", "u64").FuncParamWithName("path", "dentry", "d_inode", "i_ino"), + tkbtf.NewFetchArg("fdmj", tkbtf.BitFieldTypeMask(devMajor)).FuncParamWithName("path", "dentry", "d_inode", "i_sb", "s_dev"), + tkbtf.NewFetchArg("fdmn", tkbtf.BitFieldTypeMask(devMinor)).FuncParamWithName("path", "dentry", "d_inode", "i_sb", "s_dev"), + tkbtf.NewFetchArg("pdmj", tkbtf.BitFieldTypeMask(devMajor)).FuncParamWithName("path", "dentry", "d_parent", "d_inode", "i_sb", "s_dev"), + tkbtf.NewFetchArg("pdmn", tkbtf.BitFieldTypeMask(devMinor)).FuncParamWithName("path", "dentry", "d_parent", "d_inode", "i_sb", "s_dev"), + tkbtf.NewFetchArg("fn", "string").FuncParamWithName("path", "dentry", "d_name", "name"), + ).SetFilter(f.filter) + + btfSymbol := tkbtf.NewSymbol(f.symbolName).AddProbes(probe) + + if err := spec.BuildSymbol(btfSymbol); err != nil { + return nil, err + } + + return []*probeWithAllocFunc{ + { + probe: probe, + allocateFn: allocFunc, + }, + }, nil +} + +func (f *vfsGetAttrSymbol) onErr(_ error) bool { + return false +} diff --git a/auditbeat/module/file_integrity/kprobes/probes_vfs_getattr_test.go b/auditbeat/module/file_integrity/kprobes/probes_vfs_getattr_test.go new file mode 100644 index 00000000000..215893e81b7 --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/probes_vfs_getattr_test.go @@ -0,0 +1,147 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_vfsGetAttr_buildProbes(t *testing.T) { + specs, err := loadEmbeddedSpecs() + require.NoError(t, err) + require.NotEmpty(t, specs) + + s := &vfsGetAttrSymbol{} + + for _, spec := range specs { + switch { + case spec.ContainsSymbol("vfs_getattr_nosec"): + s.symbolName = "vfs_getattr_nosec" + case spec.ContainsSymbol("vfs_getattr"): + s.symbolName = "vfs_getattr" + default: + t.FailNow() + } + + _, err := s.buildProbes(spec) + require.NoError(t, err) + + if err != nil { + t.FailNow() + } + } +} + +func Test_vfsGetAttr_load(t *testing.T) { + exec := newFixedThreadExecutor(context.TODO()) + + prbMgr := &probeManager{ + symbols: nil, + buildChecks: nil, + getSymbolInfoRuntime: nil, + } + + prbMgr.getSymbolInfoRuntime = func(symbolName string) (runtimeSymbolInfo, error) { + return runtimeSymbolInfo{}, ErrSymbolNotFound + } + require.ErrorIs(t, loadVFSGetAttrSymbol(prbMgr, exec), ErrSymbolNotFound) + + prbMgr = &probeManager{ + symbols: nil, + buildChecks: nil, + getSymbolInfoRuntime: nil, + } + prbMgr.getSymbolInfoRuntime = func(symbolName string) (runtimeSymbolInfo, error) { + if symbolName == "vfs_getattr_nosec" { + return runtimeSymbolInfo{ + symbolName: "vfs_getattr_nosec", + isOptimised: true, + optimisedSymbolName: "vfs_getattr_nosec.isra.0", + }, nil + } + return runtimeSymbolInfo{}, ErrSymbolNotFound + } + require.Error(t, loadVFSGetAttrSymbol(prbMgr, exec)) + + prbMgr = &probeManager{ + symbols: nil, + buildChecks: nil, + getSymbolInfoRuntime: nil, + } + prbMgr.getSymbolInfoRuntime = func(symbolName string) (runtimeSymbolInfo, error) { + if symbolName == "vfs_getattr" { + return runtimeSymbolInfo{ + symbolName: "vfs_getattr", + isOptimised: false, + optimisedSymbolName: "", + }, nil + } + return runtimeSymbolInfo{}, ErrSymbolNotFound + } + require.NoError(t, loadVFSGetAttrSymbol(prbMgr, exec)) + require.NotEmpty(t, prbMgr.symbols) + require.NotEmpty(t, prbMgr.buildChecks) + require.IsType(t, &vfsGetAttrSymbol{}, prbMgr.symbols[0]) + require.Equal(t, prbMgr.symbols[0].(*vfsGetAttrSymbol).symbolName, "vfs_getattr") + + prbMgr = &probeManager{ + symbols: nil, + buildChecks: nil, + getSymbolInfoRuntime: nil, + } + prbMgr.getSymbolInfoRuntime = func(symbolName string) (runtimeSymbolInfo, error) { + if symbolName == "vfs_getattr_nosec" { + return runtimeSymbolInfo{ + symbolName: "vfs_getattr_nosec", + isOptimised: false, + optimisedSymbolName: "", + }, nil + } + return runtimeSymbolInfo{}, ErrSymbolNotFound + } + require.NoError(t, loadVFSGetAttrSymbol(prbMgr, exec)) + require.NotEmpty(t, prbMgr.symbols) + require.NotEmpty(t, prbMgr.buildChecks) + require.IsType(t, &vfsGetAttrSymbol{}, prbMgr.symbols[0]) + require.Equal(t, prbMgr.symbols[0].(*vfsGetAttrSymbol).symbolName, "vfs_getattr_nosec") + + prbMgr = &probeManager{ + symbols: nil, + buildChecks: nil, + getSymbolInfoRuntime: nil, + } + unknownErr := errors.New("unknown error") + prbMgr.getSymbolInfoRuntime = func(symbolName string) (runtimeSymbolInfo, error) { + return runtimeSymbolInfo{}, unknownErr + } + require.Error(t, loadVFSGetAttrSymbol(prbMgr, exec)) +} + +func Test_vfsGetAttr_onErr(t *testing.T) { + s := &vfsGetAttrSymbol{} + + testErr := fmt.Errorf("test: %w", ErrVerifyOverlappingEvents) + repeat := s.onErr(testErr) + require.False(t, repeat) + +} From 625da6b66fc765113e5da2bc81978b7ed7d1b742 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 24 Jan 2024 09:42:08 +0200 Subject: [PATCH 07/39] feat: implement path traverser to produce monitor events by walking a path --- .../module/file_integrity/kprobes/errors.go | 1 + .../module/file_integrity/kprobes/path.go | 322 ++++++++ .../file_integrity/kprobes/path_inotify.go | 131 ++++ .../kprobes/path_inotify_test.go | 114 +++ .../file_integrity/kprobes/path_mountpoint.go | 227 ++++++ .../kprobes/path_mountpoint_test.go | 106 +++ .../file_integrity/kprobes/path_test.go | 697 ++++++++++++++++++ 7 files changed, 1598 insertions(+) create mode 100644 auditbeat/module/file_integrity/kprobes/path.go create mode 100644 auditbeat/module/file_integrity/kprobes/path_inotify.go create mode 100644 auditbeat/module/file_integrity/kprobes/path_inotify_test.go create mode 100644 auditbeat/module/file_integrity/kprobes/path_mountpoint.go create mode 100644 auditbeat/module/file_integrity/kprobes/path_mountpoint_test.go create mode 100644 auditbeat/module/file_integrity/kprobes/path_test.go diff --git a/auditbeat/module/file_integrity/kprobes/errors.go b/auditbeat/module/file_integrity/kprobes/errors.go index fccb2149214..2760f213361 100644 --- a/auditbeat/module/file_integrity/kprobes/errors.go +++ b/auditbeat/module/file_integrity/kprobes/errors.go @@ -24,4 +24,5 @@ var ( ErrVerifyMissingEvents = errors.New("probe missing events") ErrVerifyUnexpectedEvent = errors.New("received an event that was not expected") ErrSymbolNotFound = errors.New("symbol not found") + ErrAckTimeout = errors.New("timeout") ) diff --git a/auditbeat/module/file_integrity/kprobes/path.go b/auditbeat/module/file_integrity/kprobes/path.go new file mode 100644 index 00000000000..1253349c9a4 --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/path.go @@ -0,0 +1,322 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "context" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + "sync" + "syscall" + "time" +) + +type MonitorPath struct { + fullPath string + depth uint32 + isFromMove bool + tid uint32 +} + +type pathTraverser interface { + AddPathToMonitor(ctx context.Context, path string) error + GetMonitorPath(ino uint64, major uint32, minor uint32, name string) (MonitorPath, bool) + WalkAsync(path string, depth uint32, tid uint32) + ErrC() <-chan error + Close() error +} + +type statMatch struct { + ino uint64 + major uint32 + minor uint32 + depth uint32 + fileName string + isFromMove bool + tid uint32 + fullPath string +} + +type pTraverser struct { + mtx sync.RWMutex + errC chan error + ctx context.Context + cancelFn context.CancelFunc + e executor + w inotifyWatcher + isRecursive bool + waitQueueChan chan struct{} + sMatchTimeout time.Duration + statQueue []statMatch +} + +var lstat = os.Lstat // for testing + +func newPathMonitor(ctx context.Context, exec executor, timeOut time.Duration, isRecursive bool) (*pTraverser, error) { + mWatcher, err := newInotifyWatcher() + if err != nil { + return nil, err + } + + if timeOut == 0 { + timeOut = 5 * time.Second + } + + mCtx, cancelFn := context.WithCancel(ctx) + + return &pTraverser{ + mtx: sync.RWMutex{}, + ctx: mCtx, + errC: make(chan error), + cancelFn: cancelFn, + e: exec, + w: mWatcher, + isRecursive: isRecursive, + sMatchTimeout: timeOut, + }, nil +} + +func (r *pTraverser) Close() error { + r.cancelFn() + return r.w.Close() +} + +func (r *pTraverser) GetMonitorPath(ino uint64, major uint32, minor uint32, name string) (MonitorPath, bool) { + if r.ctx.Err() != nil { + return MonitorPath{}, false + } + + r.mtx.Lock() + defer r.mtx.Unlock() + + if len(r.statQueue) == 0 { + return MonitorPath{}, false + } + + monitorPath := r.statQueue[0] + if monitorPath.ino != ino || + monitorPath.major != major || + monitorPath.minor != minor || + monitorPath.fileName != name { + return MonitorPath{}, false + } + + r.statQueue = r.statQueue[1:] + + if len(r.statQueue) == 0 && r.waitQueueChan != nil { + close(r.waitQueueChan) + r.waitQueueChan = nil + } + + return MonitorPath{ + fullPath: monitorPath.fullPath, + depth: monitorPath.depth, + isFromMove: monitorPath.isFromMove, + tid: monitorPath.tid, + }, true +} + +func readDirNames(dirName string) ([]string, error) { + f, err := os.Open(dirName) + if err != nil { + return nil, err + } + names, err := f.Readdirnames(-1) + _ = f.Close() + if err != nil { + return nil, err + } + sort.Strings(names) + return names, nil +} + +func (r *pTraverser) ErrC() <-chan error { + return r.errC +} + +func (r *pTraverser) WalkAsync(path string, depth uint32, tid uint32) { + if r.ctx.Err() != nil { + return + } + + go func() { + walkErr := r.e.Run(func() error { + return r.walk(r.ctx, path, depth, true, tid) + }) + + if walkErr == nil { + return + } + + select { + case r.errC <- walkErr: + case <-r.ctx.Done(): + } + + }() +} + +func (r *pTraverser) walkRecursive(ctx context.Context, path string, mounts mountPoints, depth uint32, isFromMove bool, tid uint32) error { + + if ctx.Err() != nil { + return ctx.Err() + } + + if r.ctx.Err() != nil { + return r.ctx.Err() + } + + if !r.isRecursive && depth > 1 { + return nil + } + + // get the mountpoint associated to this path + mnt := mounts.getMountByPath(path) + if mnt == nil { + return fmt.Errorf("could not find mount for %s", path) + } + + // add the inotify watcher if it does not exist + if _, err := r.w.Add(mnt.DeviceMajor, mnt.DeviceMinor, path); err != nil { + return err + } + + r.mtx.Lock() + info, err := lstat(path) + if err != nil { + // maybe this path got deleted/moved in the meantime + // return nil + r.mtx.Unlock() + //lint:ignore nilerr no errors returned for lstat from walkRecursive + return nil + } + + // if we are about to stat the root of the mountpoint, and the subtree has a different base + // from the base of the path (e.g. /watch [path] -> /etc/test [subtree]) + // the filename reported in the kprobe event will be "test" instead of "watch". Thus, we need to + // construct the filename based on the base name of the subtree. + mntPath := strings.Replace(path, mnt.Path, "", 1) + if !strings.HasPrefix(mntPath, mnt.Subtree) { + mntPath = filepath.Join(mnt.Subtree, mntPath) + } + + matchFileName := filepath.Base(mntPath) + + r.statQueue = append(r.statQueue, statMatch{ + ino: info.Sys().(*syscall.Stat_t).Ino, + major: mnt.DeviceMajor, + minor: mnt.DeviceMinor, + depth: depth, + fileName: matchFileName, + isFromMove: isFromMove, + tid: tid, + fullPath: path, + }) + r.mtx.Unlock() + + if !info.IsDir() { + return nil + } + + names, err := readDirNames(path) + if err != nil { + // maybe this dir got deleted/moved in the meantime + // return nil + //lint:ignore nilerr no errors returned for readDirNames from walkRecursive + return nil + } + + for _, name := range names { + filename := filepath.Join(path, name) + if err = r.walkRecursive(ctx, filename, mounts, depth+1, isFromMove, tid); err != nil { + //lint:ignore nilerr no errors returned for readDirNames from walkRecursive + return nil + } + } + return nil +} + +func (r *pTraverser) waitForWalk(ctx context.Context) error { + r.mtx.Lock() + + // statQueue is already empty, return + if len(r.statQueue) == 0 { + r.mtx.Unlock() + return nil + } + + r.waitQueueChan = make(chan struct{}) + r.mtx.Unlock() + + select { + // ctx of pTraverser is done + case <-r.ctx.Done(): + return r.ctx.Err() + // ctx of walk is done + case <-ctx.Done(): + return ctx.Err() + // statQueue is empty + case <-r.waitQueueChan: + return nil + // timeout + case <-time.After(r.sMatchTimeout): + return ErrAckTimeout + } +} + +func (r *pTraverser) walk(ctx context.Context, path string, depth uint32, isFromMove bool, tid uint32) error { + + // get a snapshot of all mountpoints + mounts, err := getAllMountPoints() + if err != nil { + return err + } + + // start walking the given path + if err := r.walkRecursive(ctx, path, mounts, depth, isFromMove, tid); err != nil { + return err + } + + // wait for the monitor queue to get empty + return r.waitForWalk(ctx) +} + +func (r *pTraverser) AddPathToMonitor(ctx context.Context, path string) error { + if r.ctx.Err() != nil { + return r.ctx.Err() + } + + if ctx.Err() != nil { + return ctx.Err() + } + + // we care about the existence of the path only in AddPathToMonitor + // walk masks out all file existence errors + _, err := lstat(path) + if err != nil { + return err + } + + // paths from AddPathToMonitor are always starting with a depth of 0 + return r.e.Run(func() error { + return r.walk(ctx, path, 0, false, 0) + }) +} diff --git a/auditbeat/module/file_integrity/kprobes/path_inotify.go b/auditbeat/module/file_integrity/kprobes/path_inotify.go new file mode 100644 index 00000000000..3e4185455f5 --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/path_inotify.go @@ -0,0 +1,131 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "errors" + "sync" + + "golang.org/x/sys/unix" +) + +type mountID struct { + major uint32 + minor uint32 +} + +type inotifyWatcher interface { + Add(devMajor uint32, devMinor uint32, mountPath string) (bool, error) + Close() error +} + +type iWatcher struct { + inotifyFD int + mounts map[mountID]struct{} + uniqueFDs map[uint32]struct{} + closed bool + mtx sync.Mutex +} + +var inotifyAddWatch = unix.InotifyAddWatch + +// newInotifyWatcher creates a new inotifyWatcher object and initializes the inotify file descriptor. +// +// It returns a pointer to the newly created inotifyWatcher object and an error if there was any. +// +// Note: Having such a inotifyWatcher is necessary for Linux kernels v5.15+ (commit +// https://lore.kernel.org/all/20210810151220.285179-5-amir73il@gmail.com/). Essentially this commit adds +// a proactive check in the inline fsnotify helpers to avoid calling fsnotify() and __fsnotify_parent() (our +// kprobes) in case there are no marks of any type (inode/sb/mount) for an inode's super block. To bypass this check, +// and always make sure that our kprobes are triggered, we use the inotifyWatcher to add an inotify watch on the +// mountpoints that we are interested in (inotify IN_MOUNT doesn't interfere with our probes). Also, it keeps track +// of the mountpoints (referenced by devmajor and devminor) that have already had an inotify watch added and does not +// add them again. +func newInotifyWatcher() (*iWatcher, error) { + fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK) + if fd == -1 { + return nil, errno + } + + return &iWatcher{ + inotifyFD: fd, + mounts: make(map[mountID]struct{}), + uniqueFDs: make(map[uint32]struct{}), + }, nil +} + +// Add adds a mount to the inotifyWatcher. +// +// It takes in the device major number, device minor number, and mount as parameters. +// It returns false if the mount with the same device major number and minor number already +// has an inotify watch added. Also, it returns an error if there was any error. +func (w *iWatcher) Add(devMajor uint32, devMinor uint32, mountPath string) (bool, error) { + w.mtx.Lock() + defer w.mtx.Unlock() + + if w.closed { + return false, errors.New("inotify watcher already closed") + } + + id := mountID{ + major: devMajor, + minor: devMinor, + } + + if _, exists := w.mounts[id]; exists { + return false, nil + } + + wd, err := inotifyAddWatch(w.inotifyFD, mountPath, unix.IN_UNMOUNT) + if err != nil { + return false, err + } + + _, fdExists := w.uniqueFDs[uint32(wd)] + if fdExists { + return false, nil + } + + w.uniqueFDs[uint32(wd)] = struct{}{} + w.mounts[id] = struct{}{} + return true, nil +} + +// Close closes the inotifyWatcher and releases any associated resources. +// +// It removes all inotify watches added. If any error occurs +// during the removal of watches, it will be accumulated and returned as a single +// error value. After removing all watches, it closes the inotify file descriptor. +func (w *iWatcher) Close() error { + w.mtx.Lock() + defer w.mtx.Unlock() + + var allErr error + for fd := range w.uniqueFDs { + if _, err := unix.InotifyRmWatch(w.inotifyFD, fd); err != nil { + allErr = errors.Join(allErr, err) + } + } + w.uniqueFDs = nil + + allErr = errors.Join(allErr, unix.Close(w.inotifyFD)) + + w.mounts = nil + + return allErr +} diff --git a/auditbeat/module/file_integrity/kprobes/path_inotify_test.go b/auditbeat/module/file_integrity/kprobes/path_inotify_test.go new file mode 100644 index 00000000000..fab167c73d6 --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/path_inotify_test.go @@ -0,0 +1,114 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/stretchr/testify/require" + "golang.org/x/sys/unix" +) + +func Test_InotifyWatcher(t *testing.T) { + if runtime.GOOS != "linux" { + t.Skip("skipping on non-linux") + } + + tmpDir, err := os.MkdirTemp("", "kprobe_unit_test") + require.NoError(t, err) + defer func() { + _ = os.RemoveAll(tmpDir) + }() + + watcher, err := newInotifyWatcher() + require.NoError(t, err) + + added, err := watcher.Add(1, 1, tmpDir) + require.NoError(t, err) + require.True(t, added) + + added, err = watcher.Add(1, 1, filepath.Join(tmpDir, "test")) + require.NoError(t, err) + require.False(t, added) + + added, err = watcher.Add(2, 2, tmpDir) + require.NoError(t, err) + require.False(t, added) + + tmpDir2, err := os.MkdirTemp("", "kprobe_unit_test") + require.NoError(t, err) + defer func() { + _ = os.RemoveAll(tmpDir2) + }() + added, err = watcher.Add(2, 2, tmpDir2) + require.NoError(t, err) + require.True(t, added) + + require.NoError(t, watcher.Close()) + + _, err = watcher.Add(1, 1, tmpDir) + require.Error(t, err) +} + +func Test_InotifyWatcher_Add_Err(t *testing.T) { + if runtime.GOOS != "linux" { + t.Skip("skipping on non-linux") + } + + watcher, err := newInotifyWatcher() + require.NoError(t, err) + + inotifyAddWatch = func(fd int, pathname string, mask uint32) (int, error) { + return -1, os.ErrInvalid + } + defer func() { + inotifyAddWatch = unix.InotifyAddWatch + }() + + _, err = watcher.Add(1, 1, "non_existent") + require.ErrorIs(t, err, os.ErrInvalid) + + require.NoError(t, watcher.Close()) +} + +func Test_InotifyWatcher_Close_Err(t *testing.T) { + if runtime.GOOS != "linux" { + t.Skip("skipping on non-linux") + } + + tmpDir, err := os.MkdirTemp("", "kprobe_unit_test") + require.NoError(t, err) + defer func() { + _ = os.RemoveAll(tmpDir) + }() + + watcher, err := newInotifyWatcher() + require.NoError(t, err) + + added, err := watcher.Add(1, 1, tmpDir) + require.NoError(t, err) + require.True(t, added) + + err = os.RemoveAll(tmpDir) + require.NoError(t, err) + + require.Error(t, watcher.Close()) +} diff --git a/auditbeat/module/file_integrity/kprobes/path_mountpoint.go b/auditbeat/module/file_integrity/kprobes/path_mountpoint.go new file mode 100644 index 00000000000..33ccebca65d --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/path_mountpoint.go @@ -0,0 +1,227 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "bufio" + "fmt" + "io" + "os" + "sort" + "strconv" + "strings" + "sync" +) + +var ( + // Used to make the mount functions thread safe + mountMutex sync.Mutex +) + +// mount contains information for a specific mounted filesystem. +// +// Path - Absolute path where the directory is mounted +// FilesystemType - Type of the mounted filesystem, e.g. "ext4" +// Device - Device for filesystem (empty string if we cannot find one) +// DeviceMajor - Device major number of the filesystem. This is set even if +// Device isn't, since all filesystems have a device +// number assigned by the kernel, even pseudo-filesystems. +// DeviceMinor - Device minor number of the filesystem. This is set even if +// Device isn't, since all filesystems have a device +// number assigned by the kernel, even pseudo-filesystems. +// Subtree - The mounted subtree of the filesystem. This is usually +// "/", meaning that the entire filesystem is mounted, but +// it can differ for bind mounts. +// ReadOnly - True if this is a read-only mount +type mount struct { + Path string + FilesystemType string + DeviceMajor uint32 + DeviceMinor uint32 + Subtree string + ReadOnly bool +} + +// mountPoints allows mounts to be sorted by Path length. +type mountPoints []*mount + +func (p mountPoints) Len() int { return len(p) } +func (p mountPoints) Swap(i, j int) { p[i], p[j] = p[j], p[i] } +func (p mountPoints) Less(i, j int) bool { + if len(p[i].Path) == len(p[j].Path) { + return p[i].Path > p[j].Path + } + + return len(p[i].Path) > len(p[j].Path) +} + +// getMountByPath returns the mount point that matches the given path. +// +// The path parameter specifies the path to search for a matching mount point. +// It should not be empty. +// +// The function returns a pointer to a mount struct if a matching mount point is found, +// otherwise it returns nil. +func (p mountPoints) getMountByPath(path string) *mount { + if path == "" { + return nil + } + + // Remove trailing slash if it not root / + if len(path) > 1 && path[len(path)-1] == '/' { + path = path[:len(path)-1] + } + + for _, mount := range p { + mountPath := mount.Path + if strings.HasPrefix(path, mountPath) { + return mount + } + } + + return nil +} + +// Unescape octal-encoded escape sequences in a string from the mountinfo file. +// The kernel encodes the ' ', '\t', '\n', and '\\' bytes this way. This +// function exactly inverts what the kernel does, including by preserving +// invalid UTF-8. +func unescapeString(str string) string { + var sb strings.Builder + for i := 0; i < len(str); i++ { + b := str[i] + if b == '\\' && i+3 < len(str) { + if parsed, err := strconv.ParseInt(str[i+1:i+4], 8, 8); err == nil { + b = uint8(parsed) + i += 3 + } + } + sb.WriteByte(b) + } + return sb.String() +} + +// Parse one line of /proc/self/mountinfo. +// +// The line contains the following space-separated fields: +// +// [0] mount ID +// [1] parent ID +// [2] major:minor +// [3] root +// [4] mount point +// [5] mount options +// [6...n-1] optional field(s) +// [n] separator +// [n+1] filesystem type +// [n+2] mount source +// [n+3] super options +// +// For more details, see https://www.kernel.org/doc/Documentation/filesystems/proc.txt +func parseMountInfoLine(line string) *mount { + fields := strings.Split(line, " ") + if len(fields) < 10 { + return nil + } + + // Count the optional fields. In case new fields are appended later, + // don't simply assume that n == len(fields) - 4. + n := 6 + for fields[n] != "-" { + n++ + if n >= len(fields) { + return nil + } + } + if n+3 >= len(fields) { + return nil + } + + mnt := &mount{} + var err error + mnt.DeviceMajor, mnt.DeviceMinor, err = newDeviceMajorMinorFromString(fields[2]) + if err != nil { + return nil + } + mnt.Subtree = unescapeString(fields[3]) + mnt.Path = unescapeString(fields[4]) + for _, opt := range strings.Split(fields[5], ",") { + if opt == "ro" { + mnt.ReadOnly = true + } + } + mnt.FilesystemType = unescapeString(fields[n+1]) + return mnt +} + +// readMountInfo reads mount information from the given input reader and returns +// a list of mount points and an error. Each mount point is represented by a mount +// struct containing information about the mount. +func readMountInfo(r io.Reader) (mountPoints, error) { + seenMountsByPath := make(map[string]*mount) + var mPoints mountPoints + + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + mnt := parseMountInfoLine(line) + if mnt == nil { + continue + } + + _, exists := seenMountsByPath[mnt.Path] + if exists { + // duplicate mountpoint entries have been observed for + // /proc/sys/fs/binfmt_misc + continue + } + + mPoints = append(mPoints, mnt) + // Note this overrides the info if we have seen the mountpoint + // earlier in the file. This is correct behavior because the + // mountpoints are listed in mount order. + seenMountsByPath[mnt.Path] = mnt + } + sort.Sort(mPoints) + + return mPoints, nil +} + +// getAllMountPoints populates the mount mappings by parsing /proc/self/mountinfo. +func getAllMountPoints() (mountPoints, error) { + mountMutex.Lock() + defer mountMutex.Unlock() + + file, err := os.Open("/proc/self/mountinfo") + if err != nil { + return nil, err + } + defer func() { + _ = file.Close() + }() + return readMountInfo(file) +} + +// newDeviceMajorMinorFromString generates a new device major and minor numbers from a given string. +func newDeviceMajorMinorFromString(str string) (uint32, uint32, error) { + var major, minor uint32 + if count, _ := fmt.Sscanf(str, "%d:%d", &major, &minor); count != 2 { + return 0, 0, fmt.Errorf("invalid device number string %q", str) + } + return major, minor, nil +} diff --git a/auditbeat/module/file_integrity/kprobes/path_mountpoint_test.go b/auditbeat/module/file_integrity/kprobes/path_mountpoint_test.go new file mode 100644 index 00000000000..314c4a11630 --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/path_mountpoint_test.go @@ -0,0 +1,106 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_readMountInfo(t *testing.T) { + procContents := `19 42 0:19 / /sys rw,nosuid,nodev,noexec,relatime shared:6 - sysfs sysfs rw +42 0 252:1 / /etc/test/test rw,noatime shared:1 - xfs /dev/vda1 rw,attr2,inode64,noquota +20 42 0:4 / /proc rw,nosuid,nodev,noexec,relatime shared:5 - proc proc rw +23 21 0:20 / /dev/shm rw,nosuid,nodev shared:3 - tmpfs tmpfs rw +25 42 0:22 / /run rw,nosuid,nodev shared:23 - tmpfs tmpfs rw,mode=755 +26 19 0:23 / /sys/fs/cgroup ro,nosuid,nodev,noexec shared:8 - tmpfs tmpfs ro,mode=755 +42 0 252:1 / / rw,noatime shared:1 - xfs /dev/vda1 rw,attr2,inode64,noquota +45 19 0:8 / /sys/kernel/debug rw,relatime shared:26 - debugfs debugfs rw +46 20 0:39 / /proc/sys/fs/binfmt_misc rw,relatime shared:27 - autofs systemd-1 rw,fd=34,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=13706 +47 42 259:0 / /boot/efi rw,noatime shared:28 - vfat /dev/vda128 rw,fmask=0077,dmask=0077,codepage=437,iocharset=ascii,shortname=winnt,errors=remount-ro +42 0 252:1 / /etc/test rw,noatime shared:1 - xfs /dev/vda1 rw,attr2,inode64,noquota +` + + sortedPaths := []string{ + "/proc/sys/fs/binfmt_misc", + "/sys/kernel/debug", + "/sys/fs/cgroup", + "/etc/test/test", + "/etc/test", + "/boot/efi", + "/dev/shm", + "/proc", + "/sys", + "/run", + "/", + } + + reader := strings.NewReader(procContents) + + mounts, err := readMountInfo(reader) + require.NoError(t, err) + require.Len(t, mounts, 11) + + for i, path := range sortedPaths { + require.Equal(t, path, mounts[i].Path) + } + + require.Equal(t, mounts[10], &mount{ + Path: "/", + FilesystemType: "xfs", + DeviceMajor: 252, + DeviceMinor: 1, + Subtree: "/", + ReadOnly: false, + }) + + require.Equal(t, mounts[2], &mount{ + Path: "/sys/fs/cgroup", + FilesystemType: "tmpfs", + DeviceMajor: 0, + DeviceMinor: 23, + Subtree: "/", + ReadOnly: true, + }) + + require.Equal(t, mounts[0], &mount{ + Path: "/proc/sys/fs/binfmt_misc", + FilesystemType: "autofs", + DeviceMajor: 0, + DeviceMinor: 39, + Subtree: "/", + ReadOnly: false, + }) + + pathMountPoint := mounts.getMountByPath("/etc/test/") + + require.Equal(t, pathMountPoint, &mount{ + Path: "/etc/test", + FilesystemType: "xfs", + DeviceMajor: 252, + DeviceMinor: 1, + Subtree: "/", + ReadOnly: false, + }) + + pathMountPoint = mounts.getMountByPath("unknown") + + require.Nil(t, pathMountPoint) +} diff --git a/auditbeat/module/file_integrity/kprobes/path_test.go b/auditbeat/module/file_integrity/kprobes/path_test.go new file mode 100644 index 00000000000..69bdf17dbe8 --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/path_test.go @@ -0,0 +1,697 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "context" + "os" + "path/filepath" + "sync" + "syscall" + "testing" + "time" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +func Test_PathTraverser_newPathMonitor(t *testing.T) { + ctx := context.Background() + + pTrav, err := newPathMonitor(ctx, newFixedThreadExecutor(ctx), 0, true) + require.NoError(t, err) + require.Equal(t, pTrav.sMatchTimeout, 5*time.Second) + require.NoError(t, pTrav.Close()) + + pTrav, err = newPathMonitor(ctx, newFixedThreadExecutor(ctx), 2*time.Second, true) + require.NoError(t, err) + require.Equal(t, pTrav.sMatchTimeout, 2*time.Second) + require.NoError(t, pTrav.Close()) +} + +type pathTestSuite struct { + suite.Suite +} + +func Test_PathTraverser(t *testing.T) { + suite.Run(t, new(pathTestSuite)) +} + +func (p *pathTestSuite) TestContextCancelBeforeAdd() { + // cancelled parent context + ctx, cancelFn := context.WithCancel(context.Background()) + pTrav, err := newPathMonitor(ctx, newFixedThreadExecutor(ctx), 0, true) + p.Require().NoError(err) + cancelFn() + err = pTrav.AddPathToMonitor(ctx, "not-existing-path") + p.Require().ErrorIs(err, ctx.Err()) + p.Require().NoError(pTrav.Close()) + + // cancelled traverser context + ctx, cancelFn = context.WithCancel(context.Background()) + pTrav, err = newPathMonitor(ctx, newFixedThreadExecutor(ctx), 0, true) + p.Require().NoError(err) + pTrav.cancelFn() + err = pTrav.AddPathToMonitor(ctx, "not-existing-path") + p.Require().ErrorIs(err, pTrav.ctx.Err()) + p.Require().NoError(pTrav.Close()) + cancelFn() +} + +func (p *pathTestSuite) TestAddParentContextDone() { + ctx, cancelFn := context.WithCancel(context.Background()) + pTrav, err := newPathMonitor(ctx, newFixedThreadExecutor(ctx), 0, true) + p.Require().NoError(err) + cancelFn() + err = pTrav.AddPathToMonitor(ctx, "not-existing-path") + p.Require().ErrorIs(err, ctx.Err()) + p.Require().NoError(pTrav.Close()) +} + +func (p *pathTestSuite) TestRecursiveWalkAsync() { + var createdPathsOrder []string + createdPathsWithDepth := make(map[string]uint32) + tmpDir, err := os.MkdirTemp("", "kprobe_unit_test") + p.Require().NoError(err) + defer func() { + _ = os.RemoveAll(tmpDir) + }() + createdPathsWithDepth[tmpDir] = 1 + createdPathsOrder = append(createdPathsOrder, tmpDir) + + testDir := filepath.Join(tmpDir, "test_dir") + err = os.Mkdir(testDir, 0o644) + p.Require().NoError(err) + createdPathsWithDepth[testDir] = 2 + createdPathsOrder = append(createdPathsOrder, testDir) + + testDirTestFile := filepath.Join(tmpDir, "test_dir", "test_file") + f, err := os.Create(testDirTestFile) + p.Require().NoError(err) + p.Require().NoError(f.Close()) + createdPathsWithDepth[testDirTestFile] = 3 + createdPathsOrder = append(createdPathsOrder, testDirTestFile) + + testFile := filepath.Join(tmpDir, "test_file") + f, err = os.Create(testFile) + p.Require().NoError(err) + p.Require().NoError(f.Close()) + createdPathsWithDepth[testFile] = 2 + createdPathsOrder = append(createdPathsOrder, testFile) + + mounts, err := getAllMountPoints() + p.Require().NoError(err) + + p.Require().Equal(len(createdPathsOrder), len(createdPathsWithDepth)) + + expectedStatQueue := make([]statMatch, 0, len(createdPathsOrder)) + for _, path := range createdPathsOrder { + + depth, exists := createdPathsWithDepth[path] + p.Require().True(exists) + + info, err := os.Lstat(path) + p.Require().NoError(err) + mnt := mounts.getMountByPath(path) + p.Require().NotNil(mnt) + expectedStatQueue = append(expectedStatQueue, statMatch{ + ino: info.Sys().(*syscall.Stat_t).Ino, + major: mnt.DeviceMajor, + minor: mnt.DeviceMinor, + depth: depth, + fileName: info.Name(), + isFromMove: true, + tid: 2, + fullPath: path, + }) + } + + ctx := context.Background() + pTrav, err := newPathMonitor(ctx, newFixedThreadExecutor(ctx), 0, true) + p.Require().NoError(err) + defer func() { + p.Require().NoError(pTrav.Close()) + }() + + pTrav.WalkAsync(tmpDir, 1, 2) + + tries := 0 + for idx := 0; idx < len(expectedStatQueue); { + mPath, match := pTrav.GetMonitorPath( + expectedStatQueue[idx].ino, + expectedStatQueue[idx].major, + expectedStatQueue[idx].minor, + expectedStatQueue[idx].fileName, + ) + + if match { + p.Require().Equal(expectedStatQueue[idx].fullPath, mPath.fullPath) + p.Require().Equal(expectedStatQueue[idx].isFromMove, mPath.isFromMove) + p.Require().Equal(expectedStatQueue[idx].tid, mPath.tid) + p.Require().Equal(expectedStatQueue[idx].depth, mPath.depth) + + tries = 0 + idx++ + continue + } + + if tries >= 3 { + p.Require().Fail("no match found") + return + } + + time.Sleep(300 * time.Millisecond) + tries++ + } + + select { + case err = <-pTrav.errC: + default: + } + + p.Require().NoError(err) + p.Require().Empty(pTrav.statQueue) +} + +func (p *pathTestSuite) TestWalkAsyncTimeoutErr() { + + tmpDir, err := os.MkdirTemp("", "kprobe_unit_test") + p.Require().NoError(err) + defer func() { + _ = os.RemoveAll(tmpDir) + }() + + ctx := context.Background() + pTrav, err := newPathMonitor(ctx, newFixedThreadExecutor(ctx), 0, true) + p.Require().NoError(err) + defer func() { + p.Require().NoError(pTrav.Close()) + }() + + pTrav.WalkAsync(tmpDir, 1, 2) + + select { + case err = <-pTrav.errC: + case <-time.After(10 * time.Second): + p.Require().Fail("no timeout error received") + } + + p.Require().ErrorIs(err, ErrAckTimeout) +} + +func (p *pathTestSuite) TestNonRecursiveWalkAsync() { + var createdPathsOrder []string + createdPathsWithDepth := make(map[string]uint32) + tmpDir, err := os.MkdirTemp("", "kprobe_unit_test") + p.Require().NoError(err) + defer func() { + _ = os.RemoveAll(tmpDir) + }() + createdPathsWithDepth[tmpDir] = 1 + createdPathsOrder = append(createdPathsOrder, tmpDir) + + testDir := filepath.Join(tmpDir, "test_dir") + err = os.Mkdir(testDir, 0o644) + p.Require().NoError(err) + + testDirTestFile := filepath.Join(tmpDir, "test_dir", "test_file") + f, err := os.Create(testDirTestFile) + p.Require().NoError(err) + p.Require().NoError(f.Close()) + + testFile := filepath.Join(tmpDir, "test_file") + f, err = os.Create(testFile) + p.Require().NoError(err) + p.Require().NoError(f.Close()) + + mounts, err := getAllMountPoints() + p.Require().NoError(err) + + p.Require().Equal(len(createdPathsOrder), len(createdPathsWithDepth)) + + expectedStatQueue := make([]statMatch, 0, len(createdPathsOrder)) + for _, path := range createdPathsOrder { + + depth, exists := createdPathsWithDepth[path] + p.Require().True(exists) + + info, err := os.Lstat(path) + p.Require().NoError(err) + mnt := mounts.getMountByPath(path) + p.Require().NotNil(mnt) + expectedStatQueue = append(expectedStatQueue, statMatch{ + ino: info.Sys().(*syscall.Stat_t).Ino, + major: mnt.DeviceMajor, + minor: mnt.DeviceMinor, + depth: depth, + fileName: info.Name(), + isFromMove: true, + tid: 2, + fullPath: path, + }) + } + + ctx := context.Background() + pTrav, err := newPathMonitor(ctx, newFixedThreadExecutor(ctx), 0, false) + p.Require().NoError(err) + defer func() { + p.Require().NoError(pTrav.Close()) + }() + + pTrav.WalkAsync(tmpDir, 1, 2) + + tries := 0 + for idx := 0; idx < len(expectedStatQueue); { + mPath, match := pTrav.GetMonitorPath( + expectedStatQueue[idx].ino, + expectedStatQueue[idx].major, + expectedStatQueue[idx].minor, + expectedStatQueue[idx].fileName, + ) + + if match { + p.Require().Equal(expectedStatQueue[idx].fullPath, mPath.fullPath) + p.Require().Equal(expectedStatQueue[idx].isFromMove, mPath.isFromMove) + p.Require().Equal(expectedStatQueue[idx].tid, mPath.tid) + p.Require().Equal(expectedStatQueue[idx].depth, mPath.depth) + + tries = 0 + idx++ + continue + } + + if tries >= 3 { + p.Require().Fail("no match found") + return + } + + time.Sleep(300 * time.Millisecond) + tries++ + } + + select { + case err = <-pTrav.errC: + default: + } + + p.Require().NoError(err) + p.Require().Empty(pTrav.statQueue) +} + +func (p *pathTestSuite) TestAddTraverserContextCancel() { + tmpDir, err := os.MkdirTemp("", "kprobe_unit_test") + p.Require().NoError(err) + defer func() { + _ = os.RemoveAll(tmpDir) + }() + + ctx := context.Background() + pTrav, err := newPathMonitor(ctx, newFixedThreadExecutor(ctx), 10*time.Second, true) + p.Require().NoError(err) + defer func() { + p.Require().NoError(pTrav.Close()) + }() + + errChan := make(chan error) + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + errPath := pTrav.AddPathToMonitor(ctx, tmpDir) + if errPath != nil { + errChan <- errPath + } + close(errChan) + }() + + tries := 0 + for { + if tries >= 4 { + p.Require().Fail("no path was added in 5 tries") + } + if len(pTrav.statQueue) == 0 { + tries++ + time.Sleep(1 * time.Second) + continue + } + break + } + pTrav.cancelFn() + + err = <-errChan + p.Require().ErrorIs(err, pTrav.ctx.Err()) +} + +func (p *pathTestSuite) TestAddTimeout() { + tmpDir, err := os.MkdirTemp("", "kprobe_unit_test") + p.Require().NoError(err) + defer func() { + _ = os.RemoveAll(tmpDir) + }() + + ctx := context.Background() + pTrav, err := newPathMonitor(ctx, newFixedThreadExecutor(ctx), 5*time.Second, true) + p.Require().NoError(err) + defer func() { + p.Require().NoError(pTrav.Close()) + }() + + errChan := make(chan error) + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + errPath := pTrav.AddPathToMonitor(ctx, tmpDir) + if errPath != nil { + errChan <- errPath + } + close(errChan) + }() + + select { + case err = <-errChan: + case <-time.After(10 * time.Second): + p.Require().Fail("no path was added in 10 seconds") + } + p.Require().ErrorIs(err, ErrAckTimeout) +} + +func (p *pathTestSuite) TestRecursiveAdd() { + var createdPathsOrder []string + createdPathsWithDepth := make(map[string]uint32) + tmpDir, err := os.MkdirTemp("", "kprobe_unit_test") + p.Require().NoError(err) + defer func() { + _ = os.RemoveAll(tmpDir) + }() + createdPathsWithDepth[tmpDir] = 0 + createdPathsOrder = append(createdPathsOrder, tmpDir) + + testDir := filepath.Join(tmpDir, "test_dir") + err = os.Mkdir(testDir, 0o644) + p.Require().NoError(err) + createdPathsWithDepth[testDir] = 1 + createdPathsOrder = append(createdPathsOrder, testDir) + + testDirTestFile := filepath.Join(tmpDir, "test_dir", "test_file") + f, err := os.Create(testDirTestFile) + p.Require().NoError(err) + p.Require().NoError(f.Close()) + createdPathsWithDepth[testDirTestFile] = 2 + createdPathsOrder = append(createdPathsOrder, testDirTestFile) + + testFile := filepath.Join(tmpDir, "test_file") + f, err = os.Create(testFile) + p.Require().NoError(err) + p.Require().NoError(f.Close()) + createdPathsWithDepth[testFile] = 1 + createdPathsOrder = append(createdPathsOrder, testFile) + + mounts, err := getAllMountPoints() + p.Require().NoError(err) + + p.Require().Equal(len(createdPathsOrder), len(createdPathsWithDepth)) + + expectedStatQueue := make([]statMatch, 0, len(createdPathsOrder)) + for _, path := range createdPathsOrder { + + depth, exists := createdPathsWithDepth[path] + p.Require().True(exists) + + info, err := os.Lstat(path) + p.Require().NoError(err) + mnt := mounts.getMountByPath(path) + p.Require().NotNil(mnt) + expectedStatQueue = append(expectedStatQueue, statMatch{ + ino: info.Sys().(*syscall.Stat_t).Ino, + major: mnt.DeviceMajor, + minor: mnt.DeviceMinor, + depth: depth, + fileName: info.Name(), + isFromMove: false, + tid: 0, + fullPath: path, + }) + } + + ctx := context.Background() + pTrav, err := newPathMonitor(ctx, newFixedThreadExecutor(ctx), 0, true) + p.Require().NoError(err) + defer func() { + p.Require().NoError(pTrav.Close()) + }() + + errChan := make(chan error) + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + errPath := pTrav.AddPathToMonitor(ctx, tmpDir) + if errPath != nil { + errChan <- errPath + } + close(errChan) + }() + + tries := 0 + for idx := 0; idx < len(expectedStatQueue); { + mPath, match := pTrav.GetMonitorPath( + expectedStatQueue[idx].ino, + expectedStatQueue[idx].major, + expectedStatQueue[idx].minor, + expectedStatQueue[idx].fileName, + ) + + if match { + p.Require().Equal(expectedStatQueue[idx].fullPath, mPath.fullPath) + p.Require().Equal(expectedStatQueue[idx].isFromMove, mPath.isFromMove) + p.Require().Equal(expectedStatQueue[idx].tid, mPath.tid) + p.Require().Equal(expectedStatQueue[idx].depth, mPath.depth) + + tries = 0 + idx++ + continue + } + + if tries >= 3 { + p.Require().Fail("no match found") + } + + time.Sleep(100 * time.Millisecond) + tries++ + } + + err = <-errChan + p.Require().NoError(err) + p.Require().Empty(pTrav.statQueue) +} + +func (p *pathTestSuite) TestNonRecursiveAdd() { + var createdPathsOrder []string + createdPathsWithDepth := make(map[string]uint32) + tmpDir, err := os.MkdirTemp("", "kprobe_unit_test") + p.Require().NoError(err) + defer func() { + _ = os.RemoveAll(tmpDir) + }() + createdPathsWithDepth[tmpDir] = 0 + createdPathsOrder = append(createdPathsOrder, tmpDir) + + testDir := filepath.Join(tmpDir, "test_dir") + err = os.Mkdir(testDir, 0o644) + p.Require().NoError(err) + createdPathsWithDepth[testDir] = 1 + createdPathsOrder = append(createdPathsOrder, testDir) + + testDirTestFile := filepath.Join(tmpDir, "test_dir", "test_file") + f, err := os.Create(testDirTestFile) + p.Require().NoError(err) + p.Require().NoError(f.Close()) + + testFile := filepath.Join(tmpDir, "test_file") + f, err = os.Create(testFile) + p.Require().NoError(err) + p.Require().NoError(f.Close()) + createdPathsWithDepth[testFile] = 1 + createdPathsOrder = append(createdPathsOrder, testFile) + + mounts, err := getAllMountPoints() + p.Require().NoError(err) + + p.Require().Equal(len(createdPathsOrder), len(createdPathsWithDepth)) + + expectedStatQueue := make([]statMatch, 0, len(createdPathsOrder)) + for _, path := range createdPathsOrder { + + depth, exists := createdPathsWithDepth[path] + p.Require().True(exists) + + info, err := os.Lstat(path) + p.Require().NoError(err) + mnt := mounts.getMountByPath(path) + p.Require().NotNil(mnt) + expectedStatQueue = append(expectedStatQueue, statMatch{ + ino: info.Sys().(*syscall.Stat_t).Ino, + major: mnt.DeviceMajor, + minor: mnt.DeviceMinor, + depth: depth, + fileName: info.Name(), + isFromMove: false, + tid: 0, + fullPath: path, + }) + } + + ctx := context.Background() + pTrav, err := newPathMonitor(ctx, newFixedThreadExecutor(ctx), 0, false) + p.Require().NoError(err) + defer func() { + p.Require().NoError(pTrav.Close()) + }() + + errChan := make(chan error) + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + errPath := pTrav.AddPathToMonitor(ctx, tmpDir) + if errPath != nil { + errChan <- errPath + } + close(errChan) + }() + + tries := 0 + for idx := 0; idx < len(expectedStatQueue); { + mPath, match := pTrav.GetMonitorPath( + expectedStatQueue[idx].ino, + expectedStatQueue[idx].major, + expectedStatQueue[idx].minor, + expectedStatQueue[idx].fileName, + ) + + if match { + p.Require().Equal(expectedStatQueue[idx].fullPath, mPath.fullPath) + p.Require().Equal(expectedStatQueue[idx].isFromMove, mPath.isFromMove) + p.Require().Equal(expectedStatQueue[idx].tid, mPath.tid) + p.Require().Equal(expectedStatQueue[idx].depth, mPath.depth) + + tries = 0 + idx++ + continue + } + + if tries >= 3 { + p.Require().Fail("no match found") + } + + time.Sleep(100 * time.Millisecond) + tries++ + } + + err = <-errChan + p.Require().NoError(err) + p.Require().Empty(pTrav.statQueue) +} + +func (p *pathTestSuite) TestStatErrAtRootAdd() { + defer func() { + lstat = os.Lstat + }() + // lstat error at root path to monitor + lstat = func(path string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } + ctx := context.Background() + pTrav, err := newPathMonitor(ctx, newFixedThreadExecutor(ctx), 0, true) + p.Require().NoError(err) + err = pTrav.AddPathToMonitor(ctx, "not-existing-path") + p.Require().ErrorIs(err, os.ErrNotExist) + p.Require().NoError(pTrav.Close()) +} + +func (p *pathTestSuite) TestStatErrAtWalk() { + defer func() { + lstat = os.Lstat + }() + + tmpDir, err := os.MkdirTemp("", "kprobe_unit_test") + p.Require().NoError(err) + defer func() { + _ = os.RemoveAll(tmpDir) + }() + + testDir := filepath.Join(tmpDir, "test_dir") + err = os.Mkdir(testDir, 0o644) + p.Require().NoError(err) + + testDirTestFile := filepath.Join(tmpDir, "test_dir", "test_file") + f, err := os.Create(testDirTestFile) + p.Require().NoError(err) + p.Require().NoError(f.Close()) + + testFile := filepath.Join(tmpDir, "test_file") + f, err = os.Create(testFile) + p.Require().NoError(err) + p.Require().NoError(f.Close()) + + // lstat error at root path to monitor + lstat = func(path string) (os.FileInfo, error) { + info, err := os.Lstat(path) + lstat = func(name string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } + + return info, err + } + ctx := context.Background() + pTrav, err := newPathMonitor(ctx, newFixedThreadExecutor(ctx), 0, true) + p.Require().NoError(err) + err = pTrav.AddPathToMonitor(ctx, tmpDir) + p.Require().NoError(err) + p.Require().NoError(pTrav.Close()) +} + +type pathTraverserMock struct { + mock.Mock +} + +func (p *pathTraverserMock) AddPathToMonitor(ctx context.Context, path string) error { + args := p.Called(ctx, path) + return args.Error(0) +} + +func (p *pathTraverserMock) GetMonitorPath(ino uint64, major uint32, minor uint32, name string) (MonitorPath, bool) { + args := p.Called(ino, major, minor, name) + return args.Get(0).(MonitorPath), args.Bool(1) +} + +func (p *pathTraverserMock) WalkAsync(path string, depth uint32, tid uint32) { + p.Called(path, depth, tid) +} + +func (p *pathTraverserMock) ErrC() <-chan error { + args := p.Called() + return args.Get(0).(<-chan error) +} + +func (p *pathTraverserMock) Close() error { + args := p.Called() + return args.Error(0) +} From 6f35ab1e727e4954bb723f09afcdc81ac13decbf Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 24 Jan 2024 09:43:22 +0200 Subject: [PATCH 08/39] feat: implement directory entries cache --- .../file_integrity/kprobes/events_cache.go | 154 +++++ .../kprobes/events_cache_entry.go | 131 ++++ .../kprobes/events_cache_test.go | 625 ++++++++++++++++++ 3 files changed, 910 insertions(+) create mode 100644 auditbeat/module/file_integrity/kprobes/events_cache.go create mode 100644 auditbeat/module/file_integrity/kprobes/events_cache_entry.go create mode 100644 auditbeat/module/file_integrity/kprobes/events_cache_test.go diff --git a/auditbeat/module/file_integrity/kprobes/events_cache.go b/auditbeat/module/file_integrity/kprobes/events_cache.go new file mode 100644 index 00000000000..84f5018da98 --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/events_cache.go @@ -0,0 +1,154 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "path/filepath" +) + +type dEntriesIndex map[dKey]*dEntry +type dEntriesMoveIndex map[uint64]*dEntry + +type dEntryCache struct { + index dEntriesIndex + moveCache dEntriesMoveIndex +} + +func newDirEntryCache() *dEntryCache { + return &dEntryCache{ + index: make(map[dKey]*dEntry), + moveCache: make(map[uint64]*dEntry), + } +} + +// Get returns the dEntry associated with the given key. +func (d *dEntryCache) Get(key dKey) *dEntry { + entry, exists := d.index[key] + if !exists { + return nil + } + + return entry +} + +// removeRecursively removes the given entry and all its children from the dEntryCache. Note that it is +// the responsibility of the caller to release the resources associated with the entry by calling Release. +func removeRecursively(d *dEntryCache, entry *dEntry) { + for _, child := range entry.Children { + removeRecursively(d, child) + } + + delete(d.index, dKey{ + Ino: entry.Ino, + DevMajor: entry.DevMajor, + DevMinor: entry.DevMinor, + }) +} + +// Remove removes the given entry and all its children from the dEntryCache. Note that it is +// the responsibility of the caller to release the resources associated with the entry by calling +// Release on the dEntry. +func (d *dEntryCache) Remove(entry *dEntry) *dEntry { + if entry == nil { + return nil + } + + entry.Parent.RemoveChild(entry.Name) + entry.Parent = nil + + removeRecursively(d, entry) + return entry +} + +// Add adds the given dEntry to the dEntryCache. +func (d *dEntryCache) Add(entry *dEntry, parent *dEntry) { + if entry == nil { + return + } + + _ = addRecursive(d, entry, parent, parent.Path(), nil) +} + +// addRecursive recursively adds entries to the dEntryCache and calls a function on each entry's path (if specified). +// addRecursive satisfies the needs of Add and MoveTo. For the latter the caller would like to traverse all new dEntries +// added to the dEntryCache and this is done efficiently by providing a callback function. +func addRecursive(d *dEntryCache, entry *dEntry, parent *dEntry, rootPath string, cb func(path string) error) error { + var path string + if cb != nil { + path = filepath.Join(rootPath, entry.Name) + if err := cb(path); err != nil { + return err + } + } + + parent.AddChild(entry) + + d.index[dKey{ + Ino: entry.Ino, + DevMajor: entry.DevMajor, + DevMinor: entry.DevMinor, + }] = entry + + for _, child := range entry.Children { + if err := addRecursive(d, child, entry, path, cb); err != nil { + return err + } + } + + return nil +} + +// MoveFrom removes the given entry from the dEntryCache, adds it in the intermediate moveCache associating it +// with the caller process TID and returns it. It returns nil if the entry was not found in the dEntryCache. +// Note, that such as association between the entry and the caller process TID is mandatory as Move{To,From} events +// for older Linux kernel provide only the Filename of the moved file and only parent info is available. +func (d *dEntryCache) MoveFrom(tid uint64, entry *dEntry) { + if entry == nil { + return + } + + d.Remove(entry) + + d.moveCache[tid] = entry +} + +// MoveTo gets the entry associated with the given TID from the moveCache and moves it to the under the new parent +// entry. Also, supplying a callback function allows the caller to traverse all new dEntries added to the dEntryCache. +// It returns true if the entry was found in the moveCache and false otherwise. +func (d *dEntryCache) MoveTo(tid uint64, newParent *dEntry, newFileName string, cb func(path string) error) (bool, error) { + entry, exists := d.moveCache[tid] + if !exists { + return false, nil + } + + delete(d.moveCache, tid) + entry.Name = newFileName + + return true, addRecursive(d, entry, newParent, newParent.Path(), cb) +} + +// MoveClear removes the entry associated with the given TID from the moveCache. +func (d *dEntryCache) MoveClear(tid uint64) { + entry, exists := d.moveCache[tid] + if !exists { + return + } + + delete(d.moveCache, tid) + entry.Release() +} diff --git a/auditbeat/module/file_integrity/kprobes/events_cache_entry.go b/auditbeat/module/file_integrity/kprobes/events_cache_entry.go new file mode 100644 index 00000000000..0c8c3e528a3 --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/events_cache_entry.go @@ -0,0 +1,131 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import "strings" + +type dKey struct { + Ino uint64 + DevMajor uint32 + DevMinor uint32 +} + +type dEntryChildren map[string]*dEntry + +type dEntry struct { + Parent *dEntry + Depth uint32 + Children dEntryChildren + Name string + Ino uint64 + DevMajor uint32 + DevMinor uint32 +} + +func (d *dEntry) GetParent() *dEntry { + if d == nil { + return nil + } + + return d.Parent +} + +func pathRecursive(d *dEntry, buffer *strings.Builder, size int) { + nameLen := len(d.Name) + + if d.Parent == nil { + size += nameLen + buffer.Grow(size) + buffer.WriteString(d.Name) + return + } + + size += nameLen + 1 + pathRecursive(d.Parent, buffer, size) + buffer.WriteByte('/') + buffer.WriteString(d.Name) +} + +func (d *dEntry) Path() string { + if d == nil { + return "" + } + + var buffer strings.Builder + pathRecursive(d, &buffer, 0) + defer buffer.Reset() + return buffer.String() +} + +// releaseRecursive recursive func to satisfy the needs of Release. +func releaseRecursive(val *dEntry) { + for _, child := range val.Children { + releaseRecursive(child) + delete(val.Children, child.Name) + } + + val.Children = nil + val.Parent = nil +} + +// Release releases the resources associated with the given dEntry and all its children. +func (d *dEntry) Release() { + if d == nil { + return + } + + releaseRecursive(d) +} + +func (d *dEntry) RemoveChild(name string) { + if d == nil || d.Children == nil { + return + } + + delete(d.Children, name) +} + +// AddChild adds a child entry to the dEntry. +func (d *dEntry) AddChild(child *dEntry) { + if d == nil || child == nil { + return + } + + if d.Children == nil { + d.Children = make(map[string]*dEntry) + } + + child.Parent = d + child.Depth = d.Depth + 1 + + d.Children[child.Name] = child +} + +// GetChild returns the child entry with the given name, if it exists. Otherwise, nil is returned. +func (d *dEntry) GetChild(name string) *dEntry { + if d == nil || d.Children == nil { + return nil + } + + child, exists := d.Children[name] + if !exists { + return nil + } + + return child +} diff --git a/auditbeat/module/file_integrity/kprobes/events_cache_test.go b/auditbeat/module/file_integrity/kprobes/events_cache_test.go new file mode 100644 index 00000000000..7c83f964ff1 --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/events_cache_test.go @@ -0,0 +1,625 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "errors" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func (d *dEntryCache) Dump(path string) error { + fileDump, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) + if err != nil { + return err + } + + defer fileDump.Close() + + for _, entry := range d.index { + if _, err = fileDump.WriteString(entry.Path() + "\n"); err != nil { + return err + } + } + + return nil +} + +func Test_DirEntryCache_Add(t *testing.T) { + cases := []struct { + name string + parent *dEntry + children map[string]*dEntry + }{ + { + "dentry_no_children", + &dEntry{ + Depth: 0, + Name: "test", + Ino: 1, + DevMajor: 1, + DevMinor: 1, + }, + nil, + }, + { + "dentry_with_children", + &dEntry{ + Depth: 1, + Name: "test", + Ino: 1, + DevMajor: 1, + DevMinor: 1, + }, + map[string]*dEntry{ + "child1": { + Depth: 2, + Name: "child1", + Ino: 2, + DevMajor: 1, + DevMinor: 1, + }, + "child2": { + Depth: 2, + Name: "child2", + Ino: 3, + DevMajor: 1, + DevMinor: 1, + }, + }, + }, + { + // we shouldn't add nil dentries + "check_nil_dentry_add", + nil, + nil, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + cache := newDirEntryCache() + + expectedLen := 0 + if c.parent != nil { + expectedLen++ + if c.children != nil { + for _, child := range c.children { + c.parent.AddChild(child) + expectedLen++ + } + } + } + + cache.Add(c.parent, nil) + + require.Len(t, cache.index, expectedLen) + if c.parent != nil { + require.Equal(t, c.parent, cache.index[dKey{ + Ino: c.parent.Ino, + DevMajor: c.parent.DevMajor, + DevMinor: c.parent.DevMinor, + }]) + } + + if c.children != nil { + for _, child := range c.children { + require.Equal(t, child, cache.index[dKey{ + Ino: child.Ino, + DevMajor: child.DevMajor, + DevMinor: child.DevMinor, + }]) + } + } + }) + } +} + +func Test_DirEntryCache_Get(t *testing.T) { + + cases := []struct { + name string + key dKey + entry *dEntry + }{ + { + "dentry_exists", + dKey{ + Ino: 1, + DevMajor: 1, + DevMinor: 1, + }, + &dEntry{ + Depth: 1, + Parent: nil, + Children: nil, + Name: "test", + Ino: 1, + DevMajor: 1, + DevMinor: 1, + }, + }, + { + "dentry_non_existent", + dKey{ + Ino: 10000, + DevMajor: 2, + DevMinor: 3, + }, + nil, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + cache := newDirEntryCache() + cache.Add(c.entry, nil) + + cacheEntry := cache.Get(c.key) + require.Equal(t, c.entry, cacheEntry) + }) + } +} + +func Test_DirEntryCache_Remove(t *testing.T) { + cases := []struct { + name string + parent *dEntry + children dEntryChildren + childrenChildren dEntryChildren + }{ + { + "dentry_no_children", + &dEntry{ + Parent: nil, + Name: "test", + Ino: 1, + DevMajor: 1, + DevMinor: 1, + }, + nil, + nil, + }, + { + "dentry_with_children", + &dEntry{ + Parent: nil, + Name: "test", + Ino: 1, + DevMajor: 1, + DevMinor: 1, + }, + dEntryChildren{ + "child1": { + Parent: nil, + Name: "child1", + Ino: 4, + DevMajor: 1, + DevMinor: 1, + }, + "child2": { + Parent: nil, + Name: "child2", + Ino: 7, + DevMajor: 1, + DevMinor: 1, + }, + }, + nil, + }, + { + "dentry_with_children_children", + &dEntry{ + Parent: nil, + Name: "test", + Ino: 1, + DevMajor: 1, + DevMinor: 1, + }, + dEntryChildren{ + "child1": { + Parent: nil, + Name: "child1", + Ino: 4, + DevMajor: 1, + DevMinor: 1, + }, + "child2": { + Parent: nil, + Name: "child2", + Ino: 7, + DevMajor: 1, + DevMinor: 1, + }, + }, + dEntryChildren{ + "child_child1": { + Parent: nil, + Name: "child_child1", + Ino: 10, + DevMajor: 1, + DevMinor: 1, + }, + }, + }, + { + "dentry_nil", + nil, + nil, + nil, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + cache := newDirEntryCache() + cache.Add(c.parent, nil) + + if c.parent != nil { + if c.children != nil { + for _, child := range c.children { + cache.Add(child, c.parent) + } + } + + if len(c.children) > 0 && c.childrenChildren != nil { + for _, childrenChildrenParent := range c.children { + for _, child := range c.childrenChildren { + cache.Add(child, childrenChildrenParent) + } + break + } + } + } + + removedEntry := cache.Remove(c.parent) + require.Len(t, cache.index, 0) + require.Equal(t, c.parent, removedEntry) + + removedEntry.Release() + if removedEntry != nil { + require.Nil(t, removedEntry.Children) + } + }) + } +} + +func Test_DirEntryCache_MoveFrom(t *testing.T) { + cases := []struct { + name string + tid uint64 + parent *dEntry + children dEntryChildren + }{ + { + "dentry_move", + 1, + &dEntry{ + Name: "test", + Depth: 0, + Ino: 1, + DevMajor: 1, + DevMinor: 1, + }, + dEntryChildren{ + "child1": { + Name: "child1", + Ino: 4, + DevMajor: 1, + DevMinor: 1, + }, + "child2": { + Name: "child2", + Ino: 7, + DevMajor: 1, + DevMinor: 1, + }, + }, + }, + { + "dentry_nil", + 1, + nil, + nil, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + cache := newDirEntryCache() + cache.Add(c.parent, nil) + + if c.parent != nil { + if c.children != nil { + for _, child := range c.children { + cache.Add(child, c.parent) + } + } + } + + cache.MoveFrom(c.tid, c.parent) + + require.Empty(t, cache.index) + + if c.parent == nil { + require.Len(t, cache.moveCache, 0) + return + } + + require.Len(t, cache.moveCache, 1) + + moveEntry, exists := cache.moveCache[c.tid] + require.True(t, exists) + require.Equal(t, c.parent, moveEntry) + if c.children != nil { + require.NotNil(t, c.parent.Children) + for _, child := range moveEntry.Children { + require.Equal(t, c.parent.Depth+1, child.Depth) + } + } else { + require.Nil(t, c.parent.Children) + } + }) + } +} + +func Test_DirEntryCache_MoveTo(t *testing.T) { + cases := []struct { + name string + srcTid uint64 + dstTid uint64 + entry *dEntry + children dEntryChildren + targetParent *dEntry + newFileName string + pathsToSee []string + err error + }{ + { + "dentry_move", + 1, + 1, + &dEntry{ + Name: "test", + Depth: 0, + Ino: 1, + DevMajor: 1, + DevMinor: 1, + }, + dEntryChildren{ + "child1": { + Name: "child1", + Ino: 4, + DevMajor: 1, + DevMinor: 1, + }, + "child2": { + Name: "child2", + Ino: 7, + DevMajor: 1, + DevMinor: 1, + }, + }, + &dEntry{ + Name: "test2", + Depth: 0, + Ino: 10, + DevMajor: 1, + DevMinor: 1, + }, + "test3", + []string{ + "test2/test3", + "test2/test3/child1", + "test2/test3/child2", + }, + nil, + }, + { + "dentry_not_found", + 1, + 2, + &dEntry{ + Name: "test", + Depth: 0, + Ino: 1, + DevMajor: 1, + DevMinor: 1, + }, + nil, + nil, + "", + nil, + nil, + }, + { + "callback_err", + 1, + 1, + &dEntry{ + Name: "test", + Depth: 0, + Ino: 1, + DevMajor: 1, + DevMinor: 1, + }, + nil, + nil, + "", + nil, + errors.New("error"), + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var movedPaths []string + + cache := newDirEntryCache() + if c.entry != nil { + if c.children != nil { + for _, child := range c.children { + c.entry.AddChild(child) + } + } + cache.moveCache[c.srcTid] = c.entry + } + + movedEntry, err := cache.MoveTo(c.dstTid, c.targetParent, c.newFileName, func(path string) error { + if c.err != nil { + return c.err + } + + movedPaths = append(movedPaths, path) + return nil + }) + if c.err == nil { + require.Nil(t, err) + } else { + require.ErrorIs(t, err, c.err) + } + + if c.srcTid == c.dstTid { + require.True(t, movedEntry) + require.Empty(t, cache.moveCache) + } else { + require.False(t, movedEntry) + require.NotEmpty(t, cache.moveCache) + } + require.ElementsMatch(t, c.pathsToSee, movedPaths) + }) + } +} + +func Test_DirEntryCache_MoveClear(t *testing.T) { + cases := []struct { + name string + srcTid uint64 + dstTid uint64 + entry *dEntry + }{ + { + "dentry_move", + 1, + 1, + &dEntry{ + Name: "test", + Depth: 0, + Ino: 1, + DevMajor: 1, + DevMinor: 1, + }, + }, + { + "dentry_not_found", + 1, + 2, + &dEntry{ + Name: "test", + Depth: 0, + Ino: 1, + DevMajor: 1, + DevMinor: 1, + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + cache := newDirEntryCache() + if c.entry != nil { + cache.moveCache[c.srcTid] = c.entry + } + + cache.MoveClear(c.dstTid) + + if c.srcTid == c.dstTid { + require.Empty(t, cache.moveCache) + } else { + require.NotEmpty(t, cache.moveCache) + } + }) + } +} + +func Test_DirEntryCache_GetChild(t *testing.T) { + cases := []struct { + name string + entry *dEntry + children dEntryChildren + childName string + }{ + { + "dentry_with_children", + &dEntry{ + Name: "test", + Depth: 0, + Ino: 1, + DevMajor: 1, + DevMinor: 1, + }, + dEntryChildren{ + "child1": { + Name: "child1", + Ino: 4, + DevMajor: 1, + DevMinor: 1, + }, + "child2": { + Name: "child2", + Ino: 7, + DevMajor: 1, + DevMinor: 1, + }, + }, + "child1", + }, + { + "dentry_no_children", + &dEntry{ + Name: "test", + Depth: 0, + Ino: 1, + DevMajor: 1, + DevMinor: 1, + }, + nil, + "child1", + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + + for _, child := range c.children { + c.entry.AddChild(child) + } + + childEntry := c.entry.GetChild(c.childName) + + if c.children == nil { + require.Nil(t, childEntry) + } else { + require.NotNil(t, childEntry) + } + + }) + } +} From 31ec58556e6efcb949b0ca3b893977a095b307c2 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 24 Jan 2024 09:44:57 +0200 Subject: [PATCH 09/39] feat: implement event processor to process probe events and based on directory entry cache emit the respective event --- .../file_integrity/kprobes/events_process.go | 233 ++++++++++ .../kprobes/events_process_test.go | 433 ++++++++++++++++++ 2 files changed, 666 insertions(+) create mode 100644 auditbeat/module/file_integrity/kprobes/events_process.go create mode 100644 auditbeat/module/file_integrity/kprobes/events_process_test.go diff --git a/auditbeat/module/file_integrity/kprobes/events_process.go b/auditbeat/module/file_integrity/kprobes/events_process.go new file mode 100644 index 00000000000..66e02ea0f20 --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/events_process.go @@ -0,0 +1,233 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "context" + "errors" + "path/filepath" + + "golang.org/x/sys/unix" +) + +type Emitter interface { + Emit(ePath string, pid uint32, op uint32) error +} + +type eProcessor struct { + p pathTraverser + e Emitter + d *dEntryCache + isRecursive bool +} + +func newEventProcessor(p pathTraverser, e Emitter, isRecursive bool) *eProcessor { + return &eProcessor{ + p: p, + e: e, + d: newDirEntryCache(), + isRecursive: isRecursive, + } +} + +func (e *eProcessor) process(ctx context.Context, pe *ProbeEvent) error { + // after processing return the probe event to the pool + defer releaseProbeEvent(pe) + + switch { + case pe.MaskMonitor == 1: + // Monitor events are only generated by our own pathTraverser.AddPathToMonitor or + // pathTraverser.WalkAsync + + monitorPath, match := e.p.GetMonitorPath(pe.FileIno, pe.FileDevMajor, pe.FileDevMinor, pe.FileName) + if !match { + return nil + } + + entry := e.d.Get(dKey{ + Ino: pe.FileIno, + DevMajor: pe.FileDevMajor, + DevMinor: pe.FileDevMinor, + }) + + parentEntry := e.d.Get(dKey{ + Ino: pe.ParentIno, + DevMajor: pe.ParentDevMajor, + DevMinor: pe.ParentDevMinor, + }) + + if parentEntry == nil { + entry = &dEntry{ + Name: monitorPath.fullPath, + Ino: pe.FileIno, + Depth: monitorPath.depth, + DevMajor: pe.FileDevMajor, + DevMinor: pe.FileDevMinor, + } + } else { + if entry == nil { + entry = &dEntry{ + Name: pe.FileName, + Ino: pe.FileIno, + Depth: parentEntry.Depth + 1, + DevMajor: pe.FileDevMajor, + DevMinor: pe.FileDevMinor, + } + } + } + + e.d.Add(entry, parentEntry) + + if !monitorPath.isFromMove { + return nil + } + + return e.e.Emit(entry.Path(), monitorPath.tid, unix.IN_MOVED_TO) + + case pe.MaskCreate == 1: + parentEntry := e.d.Get(dKey{ + Ino: pe.ParentIno, + DevMajor: pe.ParentDevMajor, + DevMinor: pe.ParentDevMinor, + }) + + if parentEntry == nil || parentEntry.Depth >= 1 && !e.isRecursive { + return nil + } + + entry := &dEntry{ + Children: nil, + Name: pe.FileName, + Ino: pe.FileIno, + DevMajor: pe.FileDevMajor, + DevMinor: pe.FileDevMinor, + } + + e.d.Add(entry, parentEntry) + + return e.e.Emit(entry.Path(), pe.Meta.TID, unix.IN_CREATE) + + case pe.MaskModify == 1: + entry := e.d.Get(dKey{ + Ino: pe.FileIno, + DevMajor: pe.FileDevMajor, + DevMinor: pe.FileDevMinor, + }) + + if entry == nil { + return nil + } + + return e.e.Emit(entry.Path(), pe.Meta.TID, unix.IN_MODIFY) + + case pe.MaskAttrib == 1: + entry := e.d.Get(dKey{ + Ino: pe.FileIno, + DevMajor: pe.FileDevMajor, + DevMinor: pe.FileDevMinor, + }) + + if entry == nil { + return nil + } + + return e.e.Emit(entry.Path(), pe.Meta.TID, unix.IN_ATTRIB) + + case pe.MaskMoveFrom == 1: + parentEntry := e.d.Get(dKey{ + Ino: pe.ParentIno, + DevMajor: pe.ParentDevMajor, + DevMinor: pe.ParentDevMinor, + }) + + if parentEntry == nil || parentEntry.Depth >= 1 && !e.isRecursive { + e.d.MoveClear(uint64(pe.Meta.TID)) + return nil + } + + entry := parentEntry.GetChild(pe.FileName) + if entry == nil { + return nil + } + + entryPath := entry.Path() + + e.d.MoveFrom(uint64(pe.Meta.TID), entry) + + return e.e.Emit(entryPath, pe.Meta.TID, unix.IN_MOVED_FROM) + + case pe.MaskMoveTo == 1: + parentEntry := e.d.Get(dKey{ + Ino: pe.ParentIno, + DevMajor: pe.ParentDevMajor, + DevMinor: pe.ParentDevMinor, + }) + + if parentEntry == nil || parentEntry.Depth >= 1 && !e.isRecursive { + // if parentEntry is nil then this move event is not + // for a directory we monitor + e.d.MoveClear(uint64(pe.Meta.TID)) + return nil + } + + moved, err := e.d.MoveTo(uint64(pe.Meta.TID), parentEntry, pe.FileName, func(path string) error { + return e.e.Emit(path, pe.Meta.TID, unix.IN_MOVED_TO) + }) + if err != nil { + return err + } + if moved { + return nil + } + + newEntryPath := filepath.Join(parentEntry.Path(), pe.FileName) + e.p.WalkAsync(newEntryPath, parentEntry.Depth+1, pe.Meta.TID) + + return nil + + case pe.MaskDelete == 1: + parentEntry := e.d.Get(dKey{ + Ino: pe.ParentIno, + DevMajor: pe.ParentDevMajor, + DevMinor: pe.ParentDevMinor, + }) + + if parentEntry == nil || parentEntry.Depth >= 1 && !e.isRecursive { + return nil + } + + entry := parentEntry.GetChild(pe.FileName) + if entry == nil { + return nil + } + + entryPath := entry.Path() + + e.d.Remove(entry) + + if err := e.e.Emit(entryPath, pe.Meta.TID, unix.IN_DELETE); err != nil { + return err + } + + entry.Release() + + return nil + default: + return errors.New("unknown event type") + } +} diff --git a/auditbeat/module/file_integrity/kprobes/events_process_test.go b/auditbeat/module/file_integrity/kprobes/events_process_test.go new file mode 100644 index 00000000000..3d91198d0b7 --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/events_process_test.go @@ -0,0 +1,433 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "context" + "testing" + + "github.com/elastic/beats/v7/auditbeat/tracing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "golang.org/x/sys/unix" +) + +type EmitterMock struct { + mock.Mock +} + +func (e *EmitterMock) Emit(ePath string, pid uint32, op uint32) error { + args := e.Called(ePath, pid, op) + return args.Error(0) +} + +func Test_EventProcessor_process(t *testing.T) { + type emitted struct { + path string + pid uint32 + op uint32 + } + + cases := []struct { + name string + statMatches []statMatch + events []*ProbeEvent + emits []emitted + isRecursive bool + }{ + { + "recursive_processor", + []statMatch{ + { + ino: 1, + major: 1, + minor: 1, + depth: 0, + fileName: "root", + isFromMove: false, + tid: 0, + fullPath: "/root/test", + }, + { + ino: 10, + major: 1, + minor: 1, + depth: 0, + fileName: "root2", + isFromMove: false, + tid: 0, + fullPath: "/root2/test", + }, + }, + []*ProbeEvent{ + { + // shouldn't add to cache + Meta: tracing.Metadata{ + PID: 1, + TID: 1, + }, + MaskMonitor: 1, + FileName: "root", + FileIno: 1, + FileDevMajor: 100, + FileDevMinor: 100, + }, + { + // shouldn't add to cache + Meta: tracing.Metadata{ + PID: 1, + TID: 1, + }, + MaskMonitor: 1, + FileName: "root", + FileIno: 1, + FileDevMajor: 200, + FileDevMinor: 200, + }, + { + // should add to cache but no event + Meta: tracing.Metadata{ + PID: 1, + TID: 1, + }, + MaskMonitor: 1, + FileName: "root", + FileIno: 1, + FileDevMajor: 1, + FileDevMinor: 1, + }, + { + // should add to cache but no event + Meta: tracing.Metadata{ + PID: 1, + TID: 1, + }, + MaskMonitor: 1, + FileName: "root2", + FileIno: 10, + FileDevMajor: 1, + FileDevMinor: 1, + }, + { + // should emit create event + Meta: tracing.Metadata{ + PID: 1, + TID: 1, + }, + MaskCreate: 1, + ParentDevMinor: 1, + ParentIno: 1, + ParentDevMajor: 1, + FileName: "test_create", + FileIno: 2, + FileDevMajor: 1, + FileDevMinor: 1, + }, + { + // should not emit create event + Meta: tracing.Metadata{ + PID: 1, + TID: 1, + }, + MaskCreate: 1, + ParentDevMinor: 1, + ParentIno: 3, + ParentDevMajor: 1, + FileName: "test_create", + FileIno: 2, + FileDevMajor: 1, + FileDevMinor: 1, + }, + { + // should not emit modify event + Meta: tracing.Metadata{ + PID: 1, + TID: 1, + }, + MaskModify: 1, + FileIno: 3, + FileDevMajor: 1, + FileDevMinor: 1, + }, + { + // should emit modify event + Meta: tracing.Metadata{ + PID: 1, + TID: 1, + }, + MaskModify: 1, + FileIno: 2, + FileDevMajor: 1, + FileDevMinor: 1, + }, + { + // should not emit attrib event + Meta: tracing.Metadata{ + PID: 1, + TID: 1, + }, + MaskAttrib: 1, + FileIno: 3, + FileDevMajor: 1, + FileDevMinor: 1, + }, + { + // should emit attrib event + Meta: tracing.Metadata{ + PID: 1, + TID: 1, + }, + MaskAttrib: 1, + FileIno: 2, + FileDevMajor: 1, + FileDevMinor: 1, + }, + { + // should emit delete event + Meta: tracing.Metadata{ + PID: 1, + TID: 1, + }, + MaskDelete: 1, + ParentDevMinor: 1, + ParentIno: 1, + ParentDevMajor: 1, + FileName: "test_create", + }, + { + // should not emit delete event + Meta: tracing.Metadata{ + PID: 1, + TID: 1, + }, + MaskDelete: 1, + ParentDevMinor: 1, + ParentIno: 3, + ParentDevMajor: 1, + FileName: "test_create", + }, + { + // should emit create event + Meta: tracing.Metadata{ + PID: 1, + TID: 1, + }, + MaskCreate: 1, + ParentDevMinor: 1, + ParentIno: 10, + ParentDevMajor: 1, + FileName: "test_create2", + FileIno: 11, + FileDevMajor: 1, + FileDevMinor: 1, + }, + { + // should emit create event + Meta: tracing.Metadata{ + PID: 1, + TID: 1, + }, + MaskCreate: 1, + ParentDevMinor: 1, + ParentIno: 11, + ParentDevMajor: 1, + FileName: "test_child", + FileIno: 12, + FileDevMajor: 1, + FileDevMinor: 1, + }, + { + // should emit move_from event + Meta: tracing.Metadata{ + PID: 2, + TID: 2, + }, + MaskMoveFrom: 1, + ParentDevMinor: 1, + ParentIno: 10, + ParentDevMajor: 1, + FileName: "test_create2", + }, + { + // should emit two move_to events + Meta: tracing.Metadata{ + PID: 2, + TID: 2, + }, + MaskMoveTo: 1, + ParentDevMinor: 1, + ParentIno: 1, + ParentDevMajor: 1, + FileName: "test_create_moved2", + }, + { + // should emit two move_to events + Meta: tracing.Metadata{ + PID: 3, + TID: 3, + }, + MaskMoveTo: 1, + ParentDevMinor: 1, + ParentIno: 1, + ParentDevMajor: 1, + FileName: "test_create_moved_outside", + }, + }, + []emitted{ + { + path: "/root/test/test_create", + pid: 1, + op: unix.IN_CREATE, + }, + { + path: "/root/test/test_create", + pid: 1, + op: unix.IN_MODIFY, + }, + { + path: "/root/test/test_create", + pid: 1, + op: unix.IN_ATTRIB, + }, + { + path: "/root/test/test_create", + pid: 1, + op: unix.IN_DELETE, + }, + { + path: "/root2/test/test_create2", + pid: 1, + op: unix.IN_CREATE, + }, + { + path: "/root2/test/test_create2/test_child", + pid: 1, + op: unix.IN_CREATE, + }, + { + path: "/root2/test/test_create2", + pid: 2, + op: unix.IN_MOVED_FROM, + }, + { + path: "/root/test/test_create_moved2", + pid: 2, + op: unix.IN_MOVED_TO, + }, + { + path: "/root/test/test_create_moved2/test_child", + pid: 2, + op: unix.IN_MOVED_TO, + }, + { + path: "/root/test/test_create_moved_outside", + pid: 3, + op: unix.IN_MOVED_TO, + }, + }, + true, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var emittedEvents []emitted + + mockEmitter := &EmitterMock{} + mockEmitterCall := mockEmitter.On("Emit", mock.Anything, mock.Anything, mock.Anything) + mockEmitterCall.Run(func(args mock.Arguments) { + emittedEvents = append(emittedEvents, emitted{ + path: args.Get(0).(string), + pid: args.Get(1).(uint32), + op: args.Get(2).(uint32), + }) + mockEmitterCall.ReturnArguments = []any{nil} + }) + + mockPathTraverser := &pathTraverserMock{} + mockPathTraverserCall := mockPathTraverser.On("GetMonitorPath", mock.Anything, mock.Anything, mock.Anything, mock.Anything) + mockPathTraverserCall.Run(func(args mock.Arguments) { + ino := args.Get(0).(uint64) + major := args.Get(1).(uint32) + minor := args.Get(2).(uint32) + name := args.Get(3).(string) + if len(c.statMatches) == 0 { + mockPathTraverserCall.ReturnArguments = []any{MonitorPath{}, false} + return + } + + if c.statMatches[0].ino != ino || + c.statMatches[0].major != major || + c.statMatches[0].minor != minor || + c.statMatches[0].fileName != name { + mockPathTraverserCall.ReturnArguments = []any{MonitorPath{}, false} + return + } + + mockPathTraverserCall.ReturnArguments = []any{MonitorPath{ + fullPath: c.statMatches[0].fullPath, + depth: c.statMatches[0].depth, + isFromMove: c.statMatches[0].isFromMove, + tid: c.statMatches[0].tid, + }, true} + + c.statMatches = c.statMatches[1:] + }) + + mockPathTraverser.On("WalkAsync", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + + pid := args.Get(2).(uint32) + + c.statMatches = append(c.statMatches, statMatch{ + fullPath: args.Get(0).(string), + depth: args.Get(1).(uint32), + ino: 20, + major: 1, + minor: 1, + isFromMove: true, + fileName: "test_create_moved_outside", + tid: pid, + }) + + c.events = append(c.events, []*ProbeEvent{ + { + Meta: tracing.Metadata{PID: 1, TID: 1}, + MaskMonitor: 1, + ParentDevMinor: 1, + ParentIno: 1, + ParentDevMajor: 1, + FileName: "test_create_moved_outside", + FileIno: 20, + FileDevMajor: 1, + FileDevMinor: 1, + }, + }...) + }) + + eProc := newEventProcessor(mockPathTraverser, mockEmitter, c.isRecursive) + for len(c.events) > 0 { + err := eProc.process(context.TODO(), c.events[0]) + require.NoError(t, err) + c.events = c.events[1:] + } + + require.Equal(t, c.emits, emittedEvents) + }) + } +} From ea6159355a0c6c93aa3a850f068eea375855ea58 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 24 Jan 2024 09:47:00 +0200 Subject: [PATCH 10/39] feat: implement event verifier that validates that the expected sequence of generated fs events are properly emitted --- .../module/file_integrity/kprobes/errors.go | 1 + .../file_integrity/kprobes/events_verifier.go | 280 ++++++++++++++++++ .../kprobes/events_verifier_test.go | 196 ++++++++++++ 3 files changed, 477 insertions(+) create mode 100644 auditbeat/module/file_integrity/kprobes/events_verifier.go create mode 100644 auditbeat/module/file_integrity/kprobes/events_verifier_test.go diff --git a/auditbeat/module/file_integrity/kprobes/errors.go b/auditbeat/module/file_integrity/kprobes/errors.go index 2760f213361..6199a6db986 100644 --- a/auditbeat/module/file_integrity/kprobes/errors.go +++ b/auditbeat/module/file_integrity/kprobes/errors.go @@ -23,6 +23,7 @@ var ( ErrVerifyOverlappingEvents = errors.New("probe overlapping events") ErrVerifyMissingEvents = errors.New("probe missing events") ErrVerifyUnexpectedEvent = errors.New("received an event that was not expected") + ErrVerifyNoEventsToExpect = errors.New("no probe events to expect") ErrSymbolNotFound = errors.New("symbol not found") ErrAckTimeout = errors.New("timeout") ) diff --git a/auditbeat/module/file_integrity/kprobes/events_verifier.go b/auditbeat/module/file_integrity/kprobes/events_verifier.go new file mode 100644 index 00000000000..12f8e33c76f --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/events_verifier.go @@ -0,0 +1,280 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "errors" + "os" + "path/filepath" + "sync" + "time" + + "golang.org/x/sys/unix" +) + +type eventID struct { + path string + op uint32 +} + +var eventGenerators = []func(*eventsVerifier, string, string) error{ + // create file - generates 1 event + func(e *eventsVerifier, targetFilePath string, targetMovedFilePath string) error { + file, err := os.OpenFile(targetFilePath, os.O_RDWR|os.O_CREATE, 0644) + if err != nil { + return err + } + defer file.Close() + e.addEventToExpect(targetFilePath, unix.IN_CREATE) + return nil + }, + // truncate file - generates 1 event + func(e *eventsVerifier, targetFilePath string, targetMovedFilePath string) error { + if err := os.Truncate(targetFilePath, 0); err != nil { + return err + } + e.addEventToExpect(targetFilePath, unix.IN_MODIFY) + return nil + }, + // write to file - generates 1 event + func(e *eventsVerifier, targetFilePath string, targetMovedFilePath string) error { + file, err := os.OpenFile(targetFilePath, os.O_WRONLY, 0644) + if err != nil { + return err + } + defer file.Close() + if _, err := file.WriteString("test"); err != nil { + return err + } + e.addEventToExpect(targetFilePath, unix.IN_MODIFY) + return nil + }, + // change owner of file - generates 1 event + func(e *eventsVerifier, targetFilePath string, targetMovedFilePath string) error { + if err := os.Chown(targetFilePath, os.Getuid(), os.Getgid()); err != nil { + return err + } + e.addEventToExpect(targetFilePath, unix.IN_ATTRIB) + return nil + }, + // change mode of file - generates 1 event + func(e *eventsVerifier, targetFilePath string, targetMovedFilePath string) error { + if err := os.Chmod(targetFilePath, 0700); err != nil { + return err + } + e.addEventToExpect(targetFilePath, unix.IN_ATTRIB) + return nil + }, + // change times of file - generates 1 event + func(e *eventsVerifier, targetFilePath string, targetMovedFilePath string) error { + if err := unix.Utimes(targetFilePath, []unix.Timeval{ + unix.NsecToTimeval(time.Now().UnixNano()), + unix.NsecToTimeval(time.Now().UnixNano()), + }); err != nil { + return err + } + e.addEventToExpect(targetFilePath, unix.IN_ATTRIB) + return nil + }, + // add attribute to file - generates 1 event + // Note that this may fail if the filesystem doesn't support extended attributes + // This is allVerified we just skip adding the respective event to verify + func(e *eventsVerifier, targetFilePath string, targetMovedFilePath string) error { + attrName := "user.myattr" + attrValue := []byte("Hello, xattr!") + if err := unix.Setxattr(targetFilePath, attrName, attrValue, 0); err != nil { + if !errors.Is(err, unix.EOPNOTSUPP) { + return err + } + } else { + e.addEventToExpect(targetFilePath, unix.IN_ATTRIB) + } + return nil + }, + // move file - generates 2 events + func(e *eventsVerifier, targetFilePath string, targetMovedFilePath string) error { + if err := os.Rename(targetFilePath, targetMovedFilePath); err != nil { + return err + } + e.addEventToExpect(targetFilePath, unix.IN_MOVED_FROM) + e.addEventToExpect(targetMovedFilePath, unix.IN_MOVED_TO) + return nil + }, + // remove file - generates 1 event + func(e *eventsVerifier, targetFilePath string, targetMovedFilePath string) error { + if err := os.Remove(targetMovedFilePath); err != nil { + return err + } + e.addEventToExpect(targetMovedFilePath, unix.IN_DELETE) + return nil + }, + // create a directory - generates 1 event + func(e *eventsVerifier, targetFilePath string, targetMovedFilePath string) error { + if err := os.Mkdir(targetFilePath, 0600); err != nil { + return err + } + e.addEventToExpect(targetFilePath, unix.IN_CREATE) + return nil + }, + // change mode of directory - generates 1 event + func(e *eventsVerifier, targetFilePath string, targetMovedFilePath string) error { + if err := os.Chmod(targetFilePath, 0644); err != nil { + return err + } + e.addEventToExpect(targetFilePath, unix.IN_ATTRIB) + return nil + }, + // change owner of directory - generates 1 event + func(e *eventsVerifier, targetFilePath string, targetMovedFilePath string) error { + if err := os.Chown(targetFilePath, os.Getuid(), os.Getgid()); err != nil { + return err + } + e.addEventToExpect(targetFilePath, unix.IN_ATTRIB) + return nil + }, + // add attribute to directory - generates 1 event + // Note that this may fail if the filesystem doesn't support extended attributes + // This is allVerified we just skip adding the respective event to verify + func(e *eventsVerifier, targetFilePath string, targetMovedFilePath string) error { + attrName := "user.myattr" + attrValue := []byte("Hello, xattr!") + if err := unix.Setxattr(targetFilePath, attrName, attrValue, 0); err != nil { + if !errors.Is(err, unix.EOPNOTSUPP) { + return err + } + } else { + e.addEventToExpect(targetFilePath, unix.IN_ATTRIB) + } + return nil + }, + // change times of directory - generates 1 event + func(e *eventsVerifier, targetFilePath string, targetMovedFilePath string) error { + if err := unix.Utimes(targetFilePath, []unix.Timeval{ + unix.NsecToTimeval(time.Now().UnixNano()), + unix.NsecToTimeval(time.Now().UnixNano()), + }); err != nil { + return err + } + e.addEventToExpect(targetFilePath, unix.IN_ATTRIB) + return nil + }, + // move directory - generates 2 events + func(e *eventsVerifier, targetFilePath string, targetMovedFilePath string) error { + if err := os.Rename(targetFilePath, targetMovedFilePath); err != nil { + + return err + } + e.addEventToExpect(targetFilePath, unix.IN_MOVED_FROM) + e.addEventToExpect(targetMovedFilePath, unix.IN_MOVED_TO) + return nil + }, + // remove the directory - generates 1 event + func(e *eventsVerifier, targetFilePath string, targetMovedFilePath string) error { + if err := os.Remove(targetMovedFilePath); err != nil { + return err + } + e.addEventToExpect(targetMovedFilePath, unix.IN_DELETE) + return nil + }, +} + +type eventsVerifier struct { + sync.Mutex + basePath string + eventsToExpect map[eventID]int + eventsToExpectNr int +} + +func newEventsVerifier(basePath string) (*eventsVerifier, error) { + return &eventsVerifier{ + basePath: basePath, + eventsToExpect: make(map[eventID]int), + }, nil +} + +func (e *eventsVerifier) validateEvent(path string, _ uint32, op uint32) error { + e.Lock() + defer e.Unlock() + + eID := eventID{ + path: path, + op: op, + } + _, exists := e.eventsToExpect[eID] + + if !exists { + return ErrVerifyUnexpectedEvent + } + + e.eventsToExpect[eID]-- + return nil +} + +// addEventToExpect adds an event to the eventsVerifier's list of expected events. +func (e *eventsVerifier) addEventToExpect(path string, op uint32) { + e.eventsToExpectNr++ + + eID := eventID{ + path: path, + op: op, + } + _, exists := e.eventsToExpect[eID] + + if !exists { + e.eventsToExpect[eID] = 1 + return + } + + e.eventsToExpect[eID]++ +} + +func (e *eventsVerifier) GenerateEvents() error { + targetFilePath := filepath.Join(e.basePath, "validate_file") + targetMovedFilePath := targetFilePath + "_moved" + + for _, genFunc := range eventGenerators { + e.Lock() + if err := genFunc(e, targetFilePath, targetMovedFilePath); err != nil { + e.Unlock() + return err + } + e.Unlock() + } + + return nil +} + +// Verified checks that all expected events filled during GenerateEvents() are present without any missing +// or duplicated. +func (e *eventsVerifier) Verified() error { + + if e.eventsToExpectNr == 0 { + return ErrVerifyNoEventsToExpect + } + + for _, status := range e.eventsToExpect { + switch { + case status < 0: + return ErrVerifyOverlappingEvents + case status > 0: + return ErrVerifyMissingEvents + } + } + + return nil +} diff --git a/auditbeat/module/file_integrity/kprobes/events_verifier_test.go b/auditbeat/module/file_integrity/kprobes/events_verifier_test.go new file mode 100644 index 00000000000..bf9647397f1 --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/events_verifier_test.go @@ -0,0 +1,196 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "os" + "runtime" + "testing" + + "github.com/stretchr/testify/require" + "golang.org/x/sys/unix" +) + +func Test_EventsVerifier(t *testing.T) { + + type verifierEvents struct { + path string + op uint32 + } + + cases := []struct { + name string + emitErr error + verifyErr error + expectedEvents []verifierEvents + emittedEvents []verifierEvents + }{ + { + "no_error", + nil, + nil, + []verifierEvents{ + { + path: "test", + op: unix.IN_ATTRIB, + }, + { + path: "test", + op: unix.IN_MOVED_FROM, + }, + { + path: "test", + op: unix.IN_MOVED_TO, + }, + { + path: "test", + op: unix.IN_MODIFY, + }, + { + path: "test", + op: unix.IN_CREATE, + }, + { + path: "test", + op: unix.IN_DELETE, + }, + }, + []verifierEvents{ + { + path: "test", + op: unix.IN_ATTRIB, + }, + { + path: "test", + op: unix.IN_MOVED_FROM, + }, + { + path: "test", + op: unix.IN_MOVED_TO, + }, + { + path: "test", + op: unix.IN_MODIFY, + }, + { + path: "test", + op: unix.IN_CREATE, + }, + { + path: "test", + op: unix.IN_DELETE, + }, + }, + }, { + "overlapping_events", + nil, + ErrVerifyOverlappingEvents, + []verifierEvents{ + { + path: "test", + op: unix.IN_ATTRIB, + }, + }, + []verifierEvents{ + { + path: "test", + op: unix.IN_ATTRIB, + }, + { + path: "test", + op: unix.IN_ATTRIB, + }, + }, + }, { + "missing_events", + nil, + ErrVerifyMissingEvents, + []verifierEvents{ + { + path: "test", + op: unix.IN_ATTRIB, + }, + }, + nil, + }, { + "unexpected_events", + ErrVerifyUnexpectedEvent, + nil, + []verifierEvents{ + { + path: "test", + op: unix.IN_ATTRIB, + }, + }, + []verifierEvents{ + { + path: "test", + op: unix.IN_DELETE, + }, + }, + }, { + "no_events_to_expect", + nil, + ErrVerifyNoEventsToExpect, + nil, + nil, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + e, err := newEventsVerifier("") + require.NoError(t, err) + + for _, ev := range c.expectedEvents { + e.addEventToExpect(ev.path, ev.op) + } + + for _, ev := range c.emittedEvents { + require.ErrorIs(t, e.validateEvent(ev.path, 0, ev.op), c.emitErr) + if c.emitErr != nil { + return + } + } + + require.ErrorIs(t, e.Verified(), c.verifyErr) + }) + } +} + +func Test_EventsVerifier_GenerateEvents(t *testing.T) { + if runtime.GOOS != "linux" { + t.Skip("skipping on non-linux") + } + + tmpDir, err := os.MkdirTemp("", "kprobe_unit_test") + require.NoError(t, err) + + defer func() { + rmErr := os.RemoveAll(tmpDir) + require.NoError(t, rmErr) + }() + + e, err := newEventsVerifier(tmpDir) + require.NoError(t, err) + + err = e.GenerateEvents() + require.NoError(t, err) + + require.NotEmpty(t, e.eventsToExpect) +} From f58e369d927096d683e61c082ae65067dc891a31 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 24 Jan 2024 09:48:52 +0200 Subject: [PATCH 11/39] feat: add perfChannel to reduce tracing.PerfChannel boilerplate code and satisfy testing needs --- .../file_integrity/kprobes/perf_channel.go | 75 +++++++++++++++++++ .../kprobes/perf_channel_test.go | 49 ++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 auditbeat/module/file_integrity/kprobes/perf_channel.go create mode 100644 auditbeat/module/file_integrity/kprobes/perf_channel_test.go diff --git a/auditbeat/module/file_integrity/kprobes/perf_channel.go b/auditbeat/module/file_integrity/kprobes/perf_channel.go new file mode 100644 index 00000000000..46c68f9f788 --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/perf_channel.go @@ -0,0 +1,75 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "time" + + "github.com/elastic/beats/v7/auditbeat/tracing" +) + +type perfChannel interface { + C() <-chan interface{} + ErrC() <-chan error + LostC() <-chan uint64 + Run() error + Close() error +} + +func newPerfChannel(probes map[tracing.Probe]tracing.AllocateFn, ringSizeExponent int, bufferSize int, pid int) (*tracing.PerfChannel, error) { + tfs, err := tracing.NewTraceFS() + if err != nil { + return nil, err + } + + pChannel, err := tracing.NewPerfChannel( + tracing.WithTimestamp(), + tracing.WithRingSizeExponent(ringSizeExponent), + tracing.WithBufferSize(bufferSize), + tracing.WithTID(pid), + tracing.WithPollTimeout(200*time.Millisecond), + tracing.WithWakeUpEvents(500), + ) + if err != nil { + return nil, err + } + + for probe, allocFn := range probes { + _ = tfs.RemoveKProbe(probe) + + err := tfs.AddKProbe(probe) + if err != nil { + return nil, err + } + desc, err := tfs.LoadProbeFormat(probe) + if err != nil { + return nil, err + } + + decoder, err := tracing.NewStructDecoder(desc, allocFn) + if err != nil { + return nil, err + } + + if err := pChannel.MonitorProbe(desc, decoder); err != nil { + return nil, err + } + } + + return pChannel, nil +} diff --git a/auditbeat/module/file_integrity/kprobes/perf_channel_test.go b/auditbeat/module/file_integrity/kprobes/perf_channel_test.go new file mode 100644 index 00000000000..0d1a3e0d3df --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/perf_channel_test.go @@ -0,0 +1,49 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import "github.com/stretchr/testify/mock" + +type perfChannelMock struct { + mock.Mock +} + +func (p *perfChannelMock) C() <-chan interface{} { + args := p.Called() + return args.Get(0).(chan interface{}) +} + +func (p *perfChannelMock) ErrC() <-chan error { + args := p.Called() + return args.Get(0).(chan error) +} + +func (p *perfChannelMock) LostC() <-chan uint64 { + args := p.Called() + return args.Get(0).(chan uint64) +} + +func (p *perfChannelMock) Run() error { + args := p.Called() + return args.Error(0) +} + +func (p *perfChannelMock) Close() error { + args := p.Called() + return args.Error(0) +} From 0c785cab977568a19882e21fd691514ca71ea1c1 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 24 Jan 2024 09:52:56 +0200 Subject: [PATCH 12/39] feat: implement monitor that ties together path traverser, perf channel and event emitting --- .../module/file_integrity/kprobes/monitor.go | 205 ++++++++ .../file_integrity/kprobes/monitor_test.go | 461 ++++++++++++++++++ 2 files changed, 666 insertions(+) create mode 100644 auditbeat/module/file_integrity/kprobes/monitor.go create mode 100644 auditbeat/module/file_integrity/kprobes/monitor_test.go diff --git a/auditbeat/module/file_integrity/kprobes/monitor.go b/auditbeat/module/file_integrity/kprobes/monitor.go new file mode 100644 index 00000000000..f36adc61c3c --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/monitor.go @@ -0,0 +1,205 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "context" + "errors" + "fmt" + "github.com/elastic/elastic-agent-libs/logp" + "sync/atomic" +) + +type MonitorEvent struct { + Path string + PID uint32 + Op uint32 +} + +type monitorEmitter struct { + ctx context.Context + eventC chan<- MonitorEvent +} + +func newMonitorEmitter(ctx context.Context, eventC chan MonitorEvent) *monitorEmitter { + return &monitorEmitter{ + ctx: ctx, + eventC: eventC, + } +} + +func (m *monitorEmitter) Emit(ePath string, pid uint32, op uint32) error { + select { + case <-m.ctx.Done(): + return m.ctx.Err() + + case m.eventC <- MonitorEvent{ + Path: ePath, + PID: pid, + Op: op, + }: + return nil + } +} + +type Monitor struct { + eventC chan MonitorEvent + pathMonitor *pTraverser + perfChannel perfChannel + errC chan error + eProc *eProcessor + log *logp.Logger + ctx context.Context + cancelFn context.CancelFunc + running uint32 + isRecursive bool + closeErr error +} + +func newMonitor(ctx context.Context, isRecursive bool, pChannel perfChannel, exec executor) (*Monitor, error) { + + mCtx, cancelFunc := context.WithCancel(ctx) + + p, err := newPathMonitor(mCtx, exec, 0, isRecursive) + if err != nil { + cancelFunc() + return nil, err + } + + eventChannel := make(chan MonitorEvent, 512) + eProc := newEventProcessor(p, newMonitorEmitter(mCtx, eventChannel), isRecursive) + + return &Monitor{ + eventC: eventChannel, + pathMonitor: p, + perfChannel: pChannel, + errC: make(chan error, 1), + eProc: eProc, + log: logp.NewLogger("file_integrity"), + ctx: mCtx, + cancelFn: cancelFunc, + isRecursive: isRecursive, + closeErr: nil, + }, nil +} + +func (w *Monitor) Add(path string) error { + switch atomic.LoadUint32(&w.running) { + case 0: + return errors.New("monitor not started") + case 2: + return errors.New("monitor is closed") + } + + return w.pathMonitor.AddPathToMonitor(w.ctx, path) +} + +func (w *Monitor) Close() error { + if !atomic.CompareAndSwapUint32(&w.running, 1, 2) { + switch atomic.LoadUint32(&w.running) { + case 0: + // monitor hasn't started yet + atomic.StoreUint32(&w.running, 2) + default: + return nil + } + } + + w.cancelFn() + var allErr error + allErr = errors.Join(allErr, w.pathMonitor.Close()) + allErr = errors.Join(allErr, w.perfChannel.Close()) + + return allErr +} + +func (w *Monitor) EventChannel() <-chan MonitorEvent { + return w.eventC +} + +func (w *Monitor) ErrorChannel() <-chan error { + return w.errC +} + +func (w *Monitor) writeErr(err error) { + select { + case w.errC <- err: + case <-w.ctx.Done(): + } +} + +func (w *Monitor) Start() error { + if !atomic.CompareAndSwapUint32(&w.running, 0, 1) { + return errors.New("monitor already started") + } + + if err := w.perfChannel.Run(); err != nil { + if closeErr := w.Close(); closeErr != nil { + w.log.Warnf("error at closing watcher: %v", closeErr) + } + return err + } + + go func() { + defer func() { + close(w.eventC) + if closeErr := w.Close(); closeErr != nil { + w.log.Warnf("error at closing watcher: %v", closeErr) + } + }() + + for { + select { + case <-w.ctx.Done(): + return + + case e, ok := <-w.perfChannel.C(): + if !ok { + w.writeErr(fmt.Errorf("read invalid event from perf channel")) + return + } + + switch eWithType := e.(type) { + case *ProbeEvent: + if err := w.eProc.process(w.ctx, eWithType); err != nil { + w.writeErr(err) + return + } + continue + default: + w.writeErr(errors.New("unexpected event type")) + return + } + + case err := <-w.perfChannel.ErrC(): + w.writeErr(err) + return + + case lost := <-w.perfChannel.LostC(): + w.writeErr(fmt.Errorf("events lost %d", lost)) + return + + case err := <-w.pathMonitor.ErrC(): + w.writeErr(err) + return + } + } + }() + + return nil +} diff --git a/auditbeat/module/file_integrity/kprobes/monitor_test.go b/auditbeat/module/file_integrity/kprobes/monitor_test.go new file mode 100644 index 00000000000..96809a51af6 --- /dev/null +++ b/auditbeat/module/file_integrity/kprobes/monitor_test.go @@ -0,0 +1,461 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package kprobes + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/elastic/beats/v7/auditbeat/tracing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + "golang.org/x/sys/unix" +) + +type monitorTestSuite struct { + suite.Suite +} + +func Test_Monitor(t *testing.T) { + suite.Run(t, new(monitorTestSuite)) +} + +func (p *monitorTestSuite) TestDoubleClose() { + ctx := context.Background() + mockPerfChannel := &perfChannelMock{} + mockPerfChannel.On("Close").Return(nil) + exec := newFixedThreadExecutor(ctx) + m, err := newMonitor(ctx, true, mockPerfChannel, exec) + p.Require().NoError(err) + err = m.Close() + p.Require().NoError(err) + err = m.Close() + p.Require().NoError(err) +} + +func (p *monitorTestSuite) TestPerfChannelClose() { + ctx := context.Background() + mockPerfChannel := &perfChannelMock{} + closeErr := errors.New("error closing perf channel") + mockPerfChannel.On("Close").Return(closeErr) + exec := newFixedThreadExecutor(ctx) + m, err := newMonitor(ctx, true, mockPerfChannel, exec) + p.Require().NoError(err) + err = m.Close() + p.Require().ErrorIs(err, closeErr) +} + +func (p *monitorTestSuite) TestPerfChannelRunErr() { + ctx := context.Background() + mockPerfChannel := &perfChannelMock{} + runErr := errors.New("perf channel run err") + mockPerfChannel.On("Run").Return(runErr) + mockPerfChannel.On("Close").Return(nil) + + exec := newFixedThreadExecutor(ctx) + m, err := newMonitor(ctx, true, mockPerfChannel, exec) + p.Require().NoError(err) + + err = m.Start() + p.Require().Error(err, runErr) + + p.Require().NoError(m.Close()) +} + +func (p *monitorTestSuite) TestRunPerfChannelLost() { + ctx := context.Background() + + perfLost := make(chan uint64) + perfEvent := make(chan interface{}) + perfErr := make(chan error) + + mockPerfChannel := &perfChannelMock{} + mockPerfChannel.On("Run").Return(nil) + mockPerfChannel.On("Close").Return(nil) + mockPerfChannel.On("C").Return(perfEvent) + mockPerfChannel.On("ErrC").Return(perfErr) + mockPerfChannel.On("LostC").Return(perfLost) + + exec := newFixedThreadExecutor(ctx) + m, err := newMonitor(ctx, true, mockPerfChannel, exec) + p.Require().NoError(err) + + err = m.Start() + p.Require().NoError(err) + + select { + case perfLost <- 10: + case <-time.After(5 * time.Second): + p.Fail("timeout at writing perf lost") + } + + select { + case err = <-m.ErrorChannel(): + p.Require().Error(err) + case <-time.After(5 * time.Second): + p.Fail("no error received") + } + + p.Require().NoError(m.Close()) +} + +func (p *monitorTestSuite) TestRunPerfChannelErr() { + ctx := context.Background() + + perfLost := make(chan uint64) + perfEvent := make(chan interface{}) + perfErr := make(chan error) + + mockPerfChannel := &perfChannelMock{} + mockPerfChannel.On("Run").Return(nil) + mockPerfChannel.On("Close").Return(nil) + mockPerfChannel.On("C").Return(perfEvent) + mockPerfChannel.On("ErrC").Return(perfErr) + mockPerfChannel.On("LostC").Return(perfLost) + + exec := newFixedThreadExecutor(ctx) + m, err := newMonitor(ctx, true, mockPerfChannel, exec) + p.Require().NoError(err) + + err = m.Start() + p.Require().NoError(err) + + runErr := errors.New("perf channel run err") + select { + case perfErr <- runErr: + case <-time.After(5 * time.Second): + p.Fail("timeout at writing perf err") + } + + select { + case err = <-m.ErrorChannel(): + p.Require().ErrorIs(err, runErr) + case <-time.After(5 * time.Second): + p.Fail("no error received") + } + + p.Require().NoError(m.Close()) +} + +func (p *monitorTestSuite) TestRunPathErr() { + ctx := context.Background() + + perfLost := make(chan uint64) + perfEvent := make(chan interface{}) + perfErr := make(chan error) + + mockPerfChannel := &perfChannelMock{} + mockPerfChannel.On("Run").Return(nil) + mockPerfChannel.On("Close").Return(nil) + mockPerfChannel.On("C").Return(perfEvent) + mockPerfChannel.On("ErrC").Return(perfErr) + mockPerfChannel.On("LostC").Return(perfLost) + + exec := newFixedThreadExecutor(ctx) + m, err := newMonitor(ctx, true, mockPerfChannel, exec) + p.Require().NoError(err) + + err = m.Start() + p.Require().NoError(err) + + runErr := errors.New("path channel run err") + select { + case m.pathMonitor.errC <- runErr: + case <-time.After(5 * time.Second): + p.Fail("timeout at writing path err") + } + + select { + case err = <-m.ErrorChannel(): + p.Require().ErrorIs(err, runErr) + case <-time.After(5 * time.Second): + p.Fail("no error received") + } + + p.Require().NoError(m.Close()) +} + +func (p *monitorTestSuite) TestRunUnknownEventType() { + ctx := context.Background() + + type Unknown struct{} + + perfLost := make(chan uint64) + perfEvent := make(chan interface{}) + perfErr := make(chan error) + + mockPerfChannel := &perfChannelMock{} + mockPerfChannel.On("Run").Return(nil) + mockPerfChannel.On("Close").Return(nil) + mockPerfChannel.On("C").Return(perfEvent) + mockPerfChannel.On("ErrC").Return(perfErr) + mockPerfChannel.On("LostC").Return(perfLost) + + exec := newFixedThreadExecutor(ctx) + m, err := newMonitor(ctx, true, mockPerfChannel, exec) + p.Require().NoError(err) + + err = m.Start() + p.Require().NoError(err) + + select { + case perfEvent <- &Unknown{}: + case <-time.After(5 * time.Second): + p.Fail("timeout at writing perf event") + } + + select { + case err = <-m.ErrorChannel(): + p.Require().Error(err) + case <-time.After(5 * time.Second): + p.Fail("no error received") + } + + p.Require().NoError(m.Close()) +} + +func (p *monitorTestSuite) TestRunPerfCloseEventChan() { + ctx := context.Background() + + perfLost := make(chan uint64) + perfEvent := make(chan interface{}) + perfErr := make(chan error) + + mockPerfChannel := &perfChannelMock{} + mockPerfChannel.On("Run").Return(nil) + mockPerfChannel.On("Close").Return(nil) + mockPerfChannel.On("C").Return(perfEvent) + mockPerfChannel.On("ErrC").Return(perfErr) + mockPerfChannel.On("LostC").Return(perfLost) + + exec := newFixedThreadExecutor(ctx) + m, err := newMonitor(ctx, true, mockPerfChannel, exec) + p.Require().NoError(err) + + err = m.Start() + p.Require().NoError(err) + + close(perfEvent) + + select { + case err = <-m.ErrorChannel(): + p.Require().Error(err) + case <-time.After(5 * time.Second): + p.Fail("no error received") + } + + p.Require().NoError(m.Close()) +} + +func (p *monitorTestSuite) TestDoubleStart() { + ctx := context.Background() + + perfLost := make(chan uint64) + perfEvent := make(chan interface{}) + perfErr := make(chan error) + + mockPerfChannel := &perfChannelMock{} + mockPerfChannel.On("Run").Return(nil) + mockPerfChannel.On("Close").Return(nil) + mockPerfChannel.On("C").Return(perfEvent) + mockPerfChannel.On("ErrC").Return(perfErr) + mockPerfChannel.On("LostC").Return(perfLost) + + exec := newFixedThreadExecutor(ctx) + m, err := newMonitor(ctx, true, mockPerfChannel, exec) + p.Require().NoError(err) + err = m.Start() + p.Require().NoError(err) + err = m.Start() + p.Require().Error(err) + p.Require().NoError(m.Close()) +} + +func (p *monitorTestSuite) TestAddPathNotStarted() { + ctx := context.Background() + mockPerfChannel := &perfChannelMock{} + mockPerfChannel.On("Close").Return(nil) + exec := newFixedThreadExecutor(ctx) + m, err := newMonitor(ctx, true, mockPerfChannel, exec) + p.Require().NoError(err) + err = m.Add("not-exist") + p.Require().Error(err) + + p.Require().NoError(m.Close()) +} + +func (p *monitorTestSuite) TestAddPathNotClosed() { + ctx := context.Background() + + perfLost := make(chan uint64) + perfEvent := make(chan interface{}) + perfErr := make(chan error) + + mockPerfChannel := &perfChannelMock{} + mockPerfChannel.On("Run").Return(nil) + mockPerfChannel.On("Close").Return(nil) + mockPerfChannel.On("C").Return(perfEvent) + mockPerfChannel.On("ErrC").Return(perfErr) + mockPerfChannel.On("LostC").Return(perfLost) + + exec := newFixedThreadExecutor(ctx) + m, err := newMonitor(ctx, true, mockPerfChannel, exec) + p.Require().NoError(err) + err = m.Start() + p.Require().NoError(err) + + p.Require().NoError(m.Close()) + + p.Require().Error(m.Add("not-exist")) +} + +func (p *monitorTestSuite) TestRunNoError() { + ctx := context.Background() + + perfLost := make(chan uint64) + perfEvent := make(chan interface{}) + perfErr := make(chan error) + + mockPerfChannel := &perfChannelMock{} + mockPerfChannel.On("Run").Return(nil) + mockPerfChannel.On("Close").Return(nil) + mockPerfChannel.On("C").Return(perfEvent) + mockPerfChannel.On("ErrC").Return(perfErr) + mockPerfChannel.On("LostC").Return(perfLost) + + exec := newFixedThreadExecutor(ctx) + m, err := newMonitor(ctx, true, mockPerfChannel, exec) + p.Require().NoError(err) + m.eProc.d.Add(&dEntry{ + Parent: nil, + Depth: 0, + Children: nil, + Name: "/test/test", + Ino: 1, + DevMajor: 1, + DevMinor: 1, + }, nil) + + err = m.Start() + p.Require().NoError(err) + + probeEvent := &ProbeEvent{ + Meta: tracing.Metadata{ + TID: 1, + PID: 1, + }, + MaskModify: 1, + FileIno: 1, + FileDevMajor: 1, + FileDevMinor: 1, + FileName: "test", + } + + select { + case perfEvent <- probeEvent: + case <-time.After(5 * time.Second): + p.Fail("timeout on writing event to perf channel") + } + + select { + case emittedEvent := <-m.EventChannel(): + p.Require().Equal(uint32(unix.IN_MODIFY), emittedEvent.Op) + p.Require().Equal("/test/test", emittedEvent.Path) + p.Require().Equal(uint32(1), emittedEvent.PID) + case <-time.After(5 * time.Second): + p.Fail("timeout on waiting event from monitor") + } + + p.Require().NoError(m.Close()) +} + +type emitterMock struct { + mock.Mock +} + +func (e *emitterMock) Emit(ePath string, pid uint32, op uint32) error { + args := e.Called(ePath, pid, op) + return args.Error(0) +} + +func (p *monitorTestSuite) TestRunEmitError() { + ctx := context.Background() + + perfLost := make(chan uint64) + perfEvent := make(chan interface{}) + perfErr := make(chan error) + + mockPerfChannel := &perfChannelMock{} + mockPerfChannel.On("Run").Return(nil) + mockPerfChannel.On("Close").Return(nil) + mockPerfChannel.On("C").Return(perfEvent) + mockPerfChannel.On("ErrC").Return(perfErr) + mockPerfChannel.On("LostC").Return(perfLost) + + emitErr := errors.New("emit error") + mockEmitter := &emitterMock{} + mockEmitter.On("Emit", mock.Anything, mock.Anything, mock.Anything).Return(emitErr) + + exec := newFixedThreadExecutor(ctx) + m, err := newMonitor(ctx, true, mockPerfChannel, exec) + p.Require().NoError(err) + + m.eProc.e = mockEmitter + m.eProc.d.Add(&dEntry{ + Parent: nil, + Depth: 0, + Children: nil, + Name: "/test/test", + Ino: 1, + DevMajor: 1, + DevMinor: 1, + }, nil) + + err = m.Start() + p.Require().NoError(err) + + probeEvent := &ProbeEvent{ + Meta: tracing.Metadata{ + TID: 1, + PID: 1, + }, + MaskModify: 1, + FileIno: 1, + FileDevMajor: 1, + FileDevMinor: 1, + FileName: "test", + } + + select { + case perfEvent <- probeEvent: + case <-time.After(5 * time.Second): + p.Fail("timeout on writing event to perf channel") + } + + select { + case err = <-m.ErrorChannel(): + p.Require().ErrorIs(err, emitErr) + case <-time.After(5 * time.Second): + p.Fail("timeout on waiting err from monitor") + } + + p.Require().NoError(m.Close()) +} From d8bf29233403725e8a7b9e7da72327cce48a5179 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 24 Jan 2024 09:55:36 +0200 Subject: [PATCH 13/39] feat: implement probe verification at runtime and the creation of a new monitor based on these --- .../module/file_integrity/kprobes/monitor.go | 21 ++- .../file_integrity/kprobes/monitor_test.go | 142 +++++++++++++++++ .../module/file_integrity/kprobes/verifier.go | 147 ++++++++++++++++++ 3 files changed, 309 insertions(+), 1 deletion(-) diff --git a/auditbeat/module/file_integrity/kprobes/monitor.go b/auditbeat/module/file_integrity/kprobes/monitor.go index f36adc61c3c..7667baad7db 100644 --- a/auditbeat/module/file_integrity/kprobes/monitor.go +++ b/auditbeat/module/file_integrity/kprobes/monitor.go @@ -21,8 +21,11 @@ import ( "context" "errors" "fmt" - "github.com/elastic/elastic-agent-libs/logp" "sync/atomic" + "time" + + "github.com/elastic/elastic-agent-libs/logp" + "github.com/elastic/go-perf" ) type MonitorEvent struct { @@ -71,6 +74,22 @@ type Monitor struct { closeErr error } +func New(isRecursive bool) (*Monitor, error) { + ctx := context.TODO() + + validatedProbes, exec, err := getVerifiedProbes(ctx, 5*time.Second) + if err != nil { + return nil, err + } + + pChannel, err := newPerfChannel(validatedProbes, 10, 4096, perf.AllThreads) + if err != nil { + return nil, err + } + + return newMonitor(ctx, isRecursive, pChannel, exec) +} + func newMonitor(ctx context.Context, isRecursive bool, pChannel perfChannel, exec executor) (*Monitor, error) { mCtx, cancelFunc := context.WithCancel(ctx) diff --git a/auditbeat/module/file_integrity/kprobes/monitor_test.go b/auditbeat/module/file_integrity/kprobes/monitor_test.go index 96809a51af6..d425284a42d 100644 --- a/auditbeat/module/file_integrity/kprobes/monitor_test.go +++ b/auditbeat/module/file_integrity/kprobes/monitor_test.go @@ -20,12 +20,19 @@ package kprobes import ( "context" "errors" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" "testing" "time" "github.com/elastic/beats/v7/auditbeat/tracing" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "golang.org/x/sys/unix" ) @@ -459,3 +466,138 @@ func (p *monitorTestSuite) TestRunEmitError() { p.Require().NoError(m.Close()) } + +func (p *monitorTestSuite) TestNew() { + + if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" { + p.T().Skip("skipping on non-amd64/arm64") + return + } + + if runtime.GOOS != "linux" { + p.T().Skip("skipping on non-linux") + return + } + + if os.Getuid() != 0 { + p.T().Skip("skipping as non-root") + return + } + + m, err := New(true) + p.Require().NoError(err) + + p.Require().NoError(m.Start()) + p.Require().NoError(m.Close()) +} + +const kernelURL string = "https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.7.tar.xz" + +func downloadKernel(filepath string) error { + // Create the file + out, err := os.Create(filepath) + if err != nil { + return err + } + defer out.Close() + + // Get the data + req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, kernelURL, nil) + if err != nil { + return err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + // Write the body to file + _, err = io.Copy(out, resp.Body) + return err +} + +func BenchmarkMonitor(b *testing.B) { + if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" { + b.Skip("skipping on non-amd64/arm64") + return + } + + if runtime.GOOS != "linux" { + b.Skip("skipping on non-linux") + return + } + + if os.Getuid() != 0 { + b.Skip("skipping as non-root") + return + } + + tmpDir, err := os.MkdirTemp("", "kprobe_bench_test") + require.NoError(b, err) + defer os.RemoveAll(tmpDir) + + tarFilePath := filepath.Join(tmpDir, "linux-6.6.7.tar.xz") + + m, err := New(true) + require.NoError(b, err) + + errChan := make(chan error) + cancelChan := make(chan struct{}) + + seenEvents := uint64(0) + go func() { + defer close(errChan) + for { + select { + case mErr := <-m.ErrorChannel(): + select { + case errChan <- mErr: + case <-cancelChan: + return + } + case <-m.EventChannel(): + seenEvents += 1 + continue + case <-cancelChan: + return + } + } + }() + + require.NoError(b, m.Start()) + require.NoError(b, m.Add(tmpDir)) + + err = downloadKernel(tarFilePath) + + // decompress + require.NoError(b, err) + cmd := exec.Command("tar", "-xvf", "./linux-6.6.7.tar.xz") + cmd.Dir = tmpDir + err = cmd.Run() + require.NoError(b, err) + + // re-decompress; causes deletions of previous files + cmd = exec.Command("tar", "-xvf", "./linux-6.6.7.tar.xz") + cmd.Dir = tmpDir + err = cmd.Run() + require.NoError(b, err) + + time.Sleep(2 * time.Second) + close(cancelChan) + err = <-errChan + if err != nil { + require.Fail(b, err.Error()) + } + + require.NoError(b, m.Close()) + + // decompressing linux-6.6.7.tar.xz created 87082 files (includes created folder); measured with decompressing and + // running "find . | wc -l" + // so the dcache entry should contain 1 (tmpDir) + 1 (linux-6.6.7.tar.xz archive) + // + 87082 (folder + archive contents) dentries + require.Len(b, m.eProc.d.index, 87082+2) + + b.Logf("processed %d events", seenEvents) +} diff --git a/auditbeat/module/file_integrity/kprobes/verifier.go b/auditbeat/module/file_integrity/kprobes/verifier.go index cc659b6c25c..a270fc4bfb4 100644 --- a/auditbeat/module/file_integrity/kprobes/verifier.go +++ b/auditbeat/module/file_integrity/kprobes/verifier.go @@ -21,10 +21,15 @@ package kprobes import ( "bytes" + "context" "embed" "errors" "io/fs" + "os" "strings" + "time" + + "github.com/elastic/beats/v7/auditbeat/tracing" tkbtf "github.com/elastic/tk-btf" ) @@ -32,6 +37,52 @@ import ( //go:embed embed var embedBTFFolder embed.FS +func getVerifiedProbes(ctx context.Context, timeout time.Duration) (map[tracing.Probe]tracing.AllocateFn, executor, error) { + + fExec := newFixedThreadExecutor(ctx) + + probeMgr, err := newProbeManager(fExec) + if err != nil { + return nil, nil, err + } + + specs, err := loadAllSpecs() + if err != nil { + return nil, nil, err + } + + var allErr error + for len(specs) > 0 { + + s := specs[0] + if !probeMgr.shouldBuild(s) { + specs = specs[1:] + continue + } + + probes, err := probeMgr.build(s) + if err != nil { + allErr = errors.Join(allErr, err) + specs = specs[1:] + continue + } + + if err := verify(ctx, fExec, probes, timeout); err != nil { + if probeMgr.onErr(err) { + continue + } + allErr = errors.Join(allErr, err) + specs = specs[1:] + continue + } + + return probes, fExec, nil + } + + fExec.Close() + return nil, nil, errors.Join(allErr, errors.New("could not validate probes")) +} + func loadAllSpecs() ([]*tkbtf.Spec, error) { var specs []*tkbtf.Spec @@ -83,3 +134,99 @@ func loadEmbeddedSpecs() ([]*tkbtf.Spec, error) { return specs, nil } + +func verify(ctx context.Context, exec executor, probes map[tracing.Probe]tracing.AllocateFn, timeout time.Duration) error { + basePath, err := os.MkdirTemp("", "verifier") + if err != nil { + return err + } + + defer func() { + _ = os.RemoveAll(basePath) + }() + + verifier, err := newEventsVerifier(basePath) + if err != nil { + return err + } + + pChannel, err := newPerfChannel(probes, 4, 512, exec.GetTID()) + if err != nil { + return err + } + + m, err := newMonitor(ctx, true, pChannel, exec) + if err != nil { + return err + } + + defer func() { + _ = m.Close() + }() + + // start the monitor + if err := m.Start(); err != nil { + return err + } + + // spaw goroutine to send events to verifier to be verified + cancel := make(chan struct{}) + defer close(cancel) + + retC := make(chan error) + + go func() { + defer close(retC) + for { + select { + case runErr := <-m.ErrorChannel(): + retC <- runErr + return + + case ev, ok := <-m.EventChannel(): + if !ok { + retC <- errors.New("monitor closed unexpectedly") + return + } + + if err := verifier.validateEvent(ev.Path, ev.PID, ev.Op); err != nil { + retC <- err + return + } + continue + case <-time.After(timeout): + return + case <-cancel: + return + } + } + }() + + // add verify base path to monitor + if err := m.Add(basePath); err != nil { + return err + } + + // invoke verifier event generation from our executor + if err := exec.Run(verifier.GenerateEvents); err != nil { + return err + } + + // wait for either no new events arriving for timeout duration or + // ctx to be cancelled + select { + case err = <-retC: + if err != nil { + return err + } + case <-ctx.Done(): + return ctx.Err() + } + + // check that all events have been verified + if err := verifier.Verified(); err != nil { + return err + } + + return nil +} From 097aa25bb07647affe2ca8903fde3141e9e8ebf4 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 24 Jan 2024 09:59:10 +0200 Subject: [PATCH 14/39] feat: implement event reader for kprobe-based file integrity module --- auditbeat/module/file_integrity/event.go | 9 +- .../file_integrity/eventreader_kprobes.go | 182 ++++++++++++++++++ auditbeat/module/file_integrity/schema.fbs | 2 + .../module/file_integrity/schema/Source.go | 6 + 4 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 auditbeat/module/file_integrity/eventreader_kprobes.go diff --git a/auditbeat/module/file_integrity/event.go b/auditbeat/module/file_integrity/event.go index fd4d68828a4..d20ed846fbc 100644 --- a/auditbeat/module/file_integrity/event.go +++ b/auditbeat/module/file_integrity/event.go @@ -62,14 +62,19 @@ func (s Source) MarshalText() ([]byte, error) { return []byte(s.String()), nil } const ( // SourceScan identifies events triggered by a file system scan. SourceScan Source = iota - // SourceFSNotify identifies events triggered by a notification from the - // file system. + // SourceFSNotify identifies file integrity events triggered by fsnotify backend. SourceFSNotify + // SourceEBPF identifies file integrity events triggered by eBPF backend. + SourceEBPF + // SourceKProbes identifies file integrity events triggered by KProbes backend. + SourceKProbes ) var sourceNames = map[Source]string{ SourceScan: "scan", SourceFSNotify: "fsnotify", + SourceEBPF: "ebpf", + SourceKProbes: "kprobes", } // Type identifies the file type (e.g. dir, file, symlink). diff --git a/auditbeat/module/file_integrity/eventreader_kprobes.go b/auditbeat/module/file_integrity/eventreader_kprobes.go new file mode 100644 index 00000000000..6d531a4e2d6 --- /dev/null +++ b/auditbeat/module/file_integrity/eventreader_kprobes.go @@ -0,0 +1,182 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//go:build linux + +package file_integrity + +import ( + "errors" + "fmt" + "path/filepath" + "time" + + "github.com/elastic/beats/v7/auditbeat/module/file_integrity/kprobes" + + "github.com/elastic/elastic-agent-libs/logp" + + "golang.org/x/sys/unix" +) + +type kProbesReader struct { + watcher *kprobes.Monitor + config Config + eventC chan Event + log *logp.Logger + + parsers []FileParser +} + +func (r kProbesReader) Start(done <-chan struct{}) (<-chan Event, error) { + watcher, err := kprobes.New(r.config.Recursive) + if err != nil { + return nil, err + } + + r.watcher = watcher + if err := r.watcher.Start(); err != nil { + // Ensure that watcher is closed so that we don't leak watchers + r.watcher.Close() + return nil, fmt.Errorf("unable to start watcher: %w", err) + } + + queueDone := make(chan struct{}) + queueC := make(chan []*Event) + + // Launch a separate goroutine to fetch all events that happen while + // watches are being installed. + go func() { + defer close(queueC) + queueC <- r.enqueueEvents(queueDone) + }() + + // kProbes watcher needs to have the watched paths + // installed after the event consumer is started, to avoid a potential + // deadlock. Do it on all platforms for simplicity. + for _, p := range r.config.Paths { + if err := r.watcher.Add(p); err != nil { + if errors.Is(err, unix.EMFILE) { + r.log.Warnw("Failed to add watch (check the max number of "+ + "open files allowed with 'ulimit -a')", + "file_path", p, "error", err) + } else { + r.log.Warnw("Failed to add watch", "file_path", p, "error", err) + } + } + } + + close(queueDone) + events := <-queueC + + // Populate callee's event channel with the previously received events + r.eventC = make(chan Event, 1+len(events)) + for _, ev := range events { + r.eventC <- *ev + } + + go r.consumeEvents(done) + + r.log.Infow("Started kprobes watcher", + "file_path", r.config.Paths, + "recursive", r.config.Recursive) + return r.eventC, nil +} + +func (r kProbesReader) enqueueEvents(done <-chan struct{}) []*Event { + var events []*Event + for { + ev := r.nextEvent(done) + if ev == nil { + break + } + events = append(events, ev) + } + + return events +} + +func (r kProbesReader) consumeEvents(done <-chan struct{}) { + defer close(r.eventC) + defer r.watcher.Close() + + for { + ev := r.nextEvent(done) + if ev == nil { + r.log.Debug("kprobes reader terminated") + return + } + r.eventC <- *ev + } +} + +func (r kProbesReader) nextEvent(done <-chan struct{}) *Event { + for { + select { + case <-done: + return nil + + case event := <-r.watcher.EventChannel(): + if event.Path == "" || r.config.IsExcludedPath(event.Path) || + !r.config.IsIncludedPath(event.Path) { + continue + } + r.log.Debugw("Received kprobes event", + "file_path", event.Path, + "event_flags", event.Op) + + abs, err := filepath.Abs(event.Path) + if err != nil { + r.log.Errorw("Failed to obtain absolute path", + "file_path", event.Path, + "error", err, + ) + event.Path = filepath.Clean(event.Path) + } else { + event.Path = abs + } + + start := time.Now() + e := NewEvent(event.Path, kProbeTypeToAction(event.Op), SourceKProbes, + r.config.MaxFileSizeBytes, r.config.HashTypes, r.parsers) + e.rtt = time.Since(start) + + return &e + + case err := <-r.watcher.ErrorChannel(): + if err != nil { + r.log.Warnw("kprobes watcher error", "error", err) + } + } + } +} + +func kProbeTypeToAction(op uint32) Action { + switch op { + case unix.IN_CREATE, unix.IN_MOVED_TO: + return Created + case unix.IN_MODIFY: + return Updated + case unix.IN_DELETE: + return Deleted + case unix.IN_MOVED_FROM: + return Moved + case unix.IN_ATTRIB: + return AttributesModified + default: + return 0 + } +} diff --git a/auditbeat/module/file_integrity/schema.fbs b/auditbeat/module/file_integrity/schema.fbs index 9e0863f6379..4c3c3b09ffc 100644 --- a/auditbeat/module/file_integrity/schema.fbs +++ b/auditbeat/module/file_integrity/schema.fbs @@ -12,6 +12,8 @@ enum Action : ubyte (bit_flags) { enum Source : ubyte { Scan, FSNotify, + EBPF, + KProbes } enum Type : ubyte { diff --git a/auditbeat/module/file_integrity/schema/Source.go b/auditbeat/module/file_integrity/schema/Source.go index 94730ce2957..028c7190d55 100644 --- a/auditbeat/module/file_integrity/schema/Source.go +++ b/auditbeat/module/file_integrity/schema/Source.go @@ -26,16 +26,22 @@ type Source byte const ( SourceScan Source = 0 SourceFSNotify Source = 1 + SourceEBPF Source = 2 + SourceKProbes Source = 3 ) var EnumNamesSource = map[Source]string{ SourceScan: "Scan", SourceFSNotify: "FSNotify", + SourceEBPF: "EBPF", + SourceKProbes: "KProbes", } var EnumValuesSource = map[string]Source{ "Scan": SourceScan, "FSNotify": SourceFSNotify, + "EBPF": SourceEBPF, + "KProbes": SourceKProbes, } func (v Source) String() string { From c4a9d9b97d3aecf59c0ef9f6c8251856b00efdb3 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 24 Jan 2024 12:48:27 +0200 Subject: [PATCH 15/39] doc: update NOTICE.txt to include tk-btf license --- NOTICE.txt | 253 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 249 insertions(+), 4 deletions(-) diff --git a/NOTICE.txt b/NOTICE.txt index 6770b3e0bff..db9cf3671ec 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -15799,6 +15799,218 @@ Contents of probable licence file $GOMODCACHE/github.com/elastic/mito@v1.8.0/LIC limitations under the License. +-------------------------------------------------------------------------------- +Dependency : github.com/elastic/tk-btf +Version: v0.1.0 +Licence type (autodetected): Apache-2.0 +-------------------------------------------------------------------------------- + +Contents of probable licence file $GOMODCACHE/github.com/elastic/tk-btf@v0.1.0/LICENSE.txt: + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + -------------------------------------------------------------------------------- Dependency : github.com/elastic/toutoumomoma Version: v0.0.0-20221026030040-594ef30cb640 @@ -36167,6 +36379,39 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +-------------------------------------------------------------------------------- +Dependency : github.com/cilium/ebpf +Version: v0.12.3 +Licence type (autodetected): MIT +-------------------------------------------------------------------------------- + +Contents of probable licence file $GOMODCACHE/github.com/cilium/ebpf@v0.12.3/LICENSE: + +MIT License + +Copyright (c) 2017 Nathan Sweet +Copyright (c) 2018, 2019 Cloudflare +Copyright (c) 2019 Authors of Cilium + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + -------------------------------------------------------------------------------- Dependency : github.com/codegangsta/inject Version: v0.0.0-20150114235600-33e0aa1cb7c0 @@ -38172,11 +38417,11 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- Dependency : github.com/frankban/quicktest -Version: v1.14.3 +Version: v1.14.5 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/frankban/quicktest@v1.14.3/LICENSE: +Contents of probable licence file $GOMODCACHE/github.com/frankban/quicktest@v1.14.5/LICENSE: MIT License @@ -45643,11 +45888,11 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- Dependency : github.com/kr/pretty -Version: v0.3.0 +Version: v0.3.1 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/kr/pretty@v0.3.0/License: +Contents of probable licence file $GOMODCACHE/github.com/kr/pretty@v0.3.1/License: Copyright 2012 Keith Rarick From 6ca359f89112fc4ded1865a382883eef3d8dac3c Mon Sep 17 00:00:00 2001 From: Maxwell Borden Date: Tue, 30 Jan 2024 18:00:56 -0800 Subject: [PATCH 16/39] feat: add tests for non-recursive kprobe fim (#3) --- .../kprobes/events_process_test.go | 245 +++++++++++++++++- 1 file changed, 244 insertions(+), 1 deletion(-) diff --git a/auditbeat/module/file_integrity/kprobes/events_process_test.go b/auditbeat/module/file_integrity/kprobes/events_process_test.go index 3d91198d0b7..e5aa0d5a914 100644 --- a/auditbeat/module/file_integrity/kprobes/events_process_test.go +++ b/auditbeat/module/file_integrity/kprobes/events_process_test.go @@ -343,6 +343,250 @@ func Test_EventProcessor_process(t *testing.T) { }, true, }, + { + "nonrecursive_processor", + []statMatch{ + { + ino: 10, + major: 1, + minor: 1, + depth: 0, + fileName: "target_dir", + isFromMove: false, + tid: 0, + fullPath: "/target_dir", + }, + { + ino: 11, + major: 1, + minor: 1, + depth: 1, + fileName: "track_me", + isFromMove: false, + tid: 0, + fullPath: "/target_dir/track_me", + }, + { + ino: 100, + major: 1, + minor: 1, + depth: 1, + fileName: "nested", + isFromMove: false, + tid: 0, + fullPath: "/target_dir/nested", + }, + { + ino: 1000, + major: 1, + minor: 1, + depth: 2, + fileName: "deeper", + isFromMove: false, + tid: 0, + fullPath: "/target_dir/nested/deeper", + }, + }, + []*ProbeEvent{ + { + // shouldn't add to cache + Meta: tracing.Metadata{ + PID: 1, + TID: 1, + }, + MaskMonitor: 1, + FileName: "target_dir", + FileIno: 1, + FileDevMajor: 100, + FileDevMinor: 100, + }, + { + // should add to cache but no event + Meta: tracing.Metadata{ + PID: 1, + TID: 1, + }, + MaskMonitor: 1, + FileName: "target_dir", + FileIno: 10, + FileDevMajor: 1, + FileDevMinor: 1, + }, + { + // should add to cache but no event + Meta: tracing.Metadata{ + PID: 1, + TID: 1, + }, + MaskMonitor: 1, + FileName: "track_me", + FileIno: 11, + FileDevMajor: 1, + FileDevMinor: 1, + }, + { + // should add to cache but no event + Meta: tracing.Metadata{ + PID: 1, + TID: 1, + }, + MaskMonitor: 1, + FileName: "nested", + FileIno: 100, + FileDevMajor: 1, + FileDevMinor: 1, + }, + { + // shouldn't add to cache and no event + Meta: tracing.Metadata{ + PID: 1, + TID: 1, + }, + MaskMonitor: 1, + FileName: "deeper", + FileIno: 1000, + FileDevMajor: 1, + FileDevMinor: 1, + }, + { + // should emit create event + Meta: tracing.Metadata{ + PID: 1, + TID: 1, + }, + MaskCreate: 1, + ParentDevMinor: 1, + ParentIno: 10, + ParentDevMajor: 1, + FileName: "test_create", + FileIno: 12, + FileDevMajor: 1, + FileDevMinor: 1, + }, + { + // should not emit create event + Meta: tracing.Metadata{ + PID: 1, + TID: 1, + }, + MaskCreate: 1, + ParentDevMinor: 1, + ParentIno: 100, + ParentDevMajor: 1, + FileName: "test_create", + FileIno: 101, + FileDevMajor: 1, + FileDevMinor: 1, + }, + { + // should not emit modify event + Meta: tracing.Metadata{ + PID: 1, + TID: 1, + }, + MaskModify: 1, + FileIno: 101, + FileDevMajor: 1, + FileDevMinor: 1, + }, + { + // should emit modify event + Meta: tracing.Metadata{ + PID: 1, + TID: 1, + }, + MaskModify: 1, + FileIno: 12, + FileDevMajor: 1, + FileDevMinor: 1, + }, + { + // should emit modify event + Meta: tracing.Metadata{ + PID: 1, + TID: 1, + }, + MaskModify: 1, + FileIno: 11, + FileDevMajor: 1, + FileDevMinor: 1, + }, + { + // should not emit attrib event + Meta: tracing.Metadata{ + PID: 1, + TID: 1, + }, + MaskAttrib: 1, + FileIno: 101, + FileDevMajor: 1, + FileDevMinor: 1, + }, + { + // should emit attrib event + Meta: tracing.Metadata{ + PID: 1, + TID: 1, + }, + MaskAttrib: 1, + FileIno: 11, + FileDevMajor: 1, + FileDevMinor: 1, + }, + { + // should emit delete event + Meta: tracing.Metadata{ + PID: 1, + TID: 1, + }, + MaskDelete: 1, + ParentDevMinor: 1, + ParentIno: 10, + ParentDevMajor: 1, + FileName: "test_create", + }, + { + // should not emit delete event + Meta: tracing.Metadata{ + PID: 1, + TID: 1, + }, + MaskDelete: 1, + ParentDevMinor: 1, + ParentIno: 100, + ParentDevMajor: 1, + FileName: "test_create", + }, + }, + []emitted{ + { + path: "/target_dir/test_create", + pid: 1, + op: unix.IN_CREATE, + }, + { + path: "/target_dir/test_create", + pid: 1, + op: unix.IN_MODIFY, + }, + { + path: "/target_dir/track_me", + pid: 1, + op: unix.IN_MODIFY, + }, + { + path: "/target_dir/track_me", + pid: 1, + op: unix.IN_ATTRIB, + }, + { + path: "/target_dir/test_create", + pid: 1, + op: unix.IN_DELETE, + }, + }, + false, + }, } for _, c := range cases { @@ -391,7 +635,6 @@ func Test_EventProcessor_process(t *testing.T) { }) mockPathTraverser.On("WalkAsync", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { - pid := args.Get(2).(uint32) c.statMatches = append(c.statMatches, statMatch{ From 07b927bb28febc70ff07072465624ab16a268693 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 31 Jan 2024 04:03:01 +0200 Subject: [PATCH 17/39] fix: remove existing file from cache when a move operation is overwriting it --- auditbeat/module/file_integrity/kprobes/events_process.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/auditbeat/module/file_integrity/kprobes/events_process.go b/auditbeat/module/file_integrity/kprobes/events_process.go index 66e02ea0f20..32691547db2 100644 --- a/auditbeat/module/file_integrity/kprobes/events_process.go +++ b/auditbeat/module/file_integrity/kprobes/events_process.go @@ -185,6 +185,11 @@ func (e *eProcessor) process(ctx context.Context, pe *ProbeEvent) error { return nil } + if existingChild := parentEntry.GetChild(pe.FileName); existingChild != nil { + e.d.Remove(existingChild) + existingChild.Release() + } + moved, err := e.d.MoveTo(uint64(pe.Meta.TID), parentEntry, pe.FileName, func(path string) error { return e.e.Emit(path, pe.Meta.TID, unix.IN_MOVED_TO) }) From 045bf40792010e9d49cd62bdb6af5fd2067723d6 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 31 Jan 2024 04:14:37 +0200 Subject: [PATCH 18/39] feat: introduce force_backend in for file integrity auditbeat module --- auditbeat/auditbeat.reference.yml | 4 +++ .../docs/modules/file_integrity.asciidoc | 8 +++-- .../file_integrity/_meta/config.yml.tmpl | 7 +++++ .../module/file_integrity/_meta/docs.asciidoc | 8 +++-- auditbeat/module/file_integrity/config.go | 29 +++++++++++++++++++ x-pack/auditbeat/auditbeat.reference.yml | 4 +++ 6 files changed, 56 insertions(+), 4 deletions(-) diff --git a/auditbeat/auditbeat.reference.yml b/auditbeat/auditbeat.reference.yml index 883760ab410..13580d12a2c 100644 --- a/auditbeat/auditbeat.reference.yml +++ b/auditbeat/auditbeat.reference.yml @@ -92,6 +92,10 @@ auditbeat.modules: # Auditbeat will ignore files unless they match a pattern. #include_files: #- '/\.ssh($|/)' + # Select the backend which will be used to source events. + # Valid values: ebpf, kprobes, fsnotify. + # Default: fsnotify. + force_backend: fsnotify # Scan over the configured file paths at startup and send events for new or # modified files since the last time Auditbeat was running. diff --git a/auditbeat/docs/modules/file_integrity.asciidoc b/auditbeat/docs/modules/file_integrity.asciidoc index a12c4df47ca..043375aed80 100644 --- a/auditbeat/docs/modules/file_integrity.asciidoc +++ b/auditbeat/docs/modules/file_integrity.asciidoc @@ -28,8 +28,12 @@ to only send events for new or modified files. The operating system features that power this feature are as follows. -* Linux - `inotify` is used, and therefore the kernel must have inotify support. -Inotify was initially merged into the 2.6.13 Linux kernel. +* Linux - As of now, three kernel backends are supported: `ebpf`, `kprobes` and `fsnotify`. +By default, `fsnotify` is used, and therefore the kernel must have inotify support. +`Inotify` was initially merged into the 2.6.13 Linux kernel. +The `Kprobes` backend uses tracefs and supports 3.10+ kernels. +The `eBPF` backend uses modern eBPF features and supports 5.10.16+ kernels. +The preferred backend can be selected by specifying the `force_backend` config option. * macOS (Darwin) - Uses the `FSEvents` API, present since macOS 10.5. This API coalesces multiple changes to a file into a single event. {beatname_uc} translates this coalesced changes into a meaningful sequence of actions. However, diff --git a/auditbeat/module/file_integrity/_meta/config.yml.tmpl b/auditbeat/module/file_integrity/_meta/config.yml.tmpl index 588a6279eee..53dc298786e 100644 --- a/auditbeat/module/file_integrity/_meta/config.yml.tmpl +++ b/auditbeat/module/file_integrity/_meta/config.yml.tmpl @@ -55,6 +55,13 @@ #- '/\.ssh($|/)' {{- end }} + {{- if eq .GOOS "linux" }} + # Select the backend which will be used to source events. + # Valid values: ebpf, kprobes, fsnotify. + # Default: fsnotify. + force_backend: fsnotify + {{- end }} + # Scan over the configured file paths at startup and send events for new or # modified files since the last time Auditbeat was running. scan_at_start: true diff --git a/auditbeat/module/file_integrity/_meta/docs.asciidoc b/auditbeat/module/file_integrity/_meta/docs.asciidoc index 0f32ef64f93..dc40d709d49 100644 --- a/auditbeat/module/file_integrity/_meta/docs.asciidoc +++ b/auditbeat/module/file_integrity/_meta/docs.asciidoc @@ -21,8 +21,12 @@ to only send events for new or modified files. The operating system features that power this feature are as follows. -* Linux - `inotify` is used, and therefore the kernel must have inotify support. -Inotify was initially merged into the 2.6.13 Linux kernel. +* Linux - As of now, three kernel backends are supported: `ebpf`, `kprobes` and `fsnotify`. +By default, `fsnotify` is used, and therefore the kernel must have inotify support. +`Inotify` was initially merged into the 2.6.13 Linux kernel. +The `Kprobes` backend uses tracefs and supports 3.10+ kernels. +The `eBPF` backend uses modern eBPF features and supports 5.10.16+ kernels. +The preferred backend can be selected by specifying the `force_backend` config option. * macOS (Darwin) - Uses the `FSEvents` API, present since macOS 10.5. This API coalesces multiple changes to a file into a single event. {beatname_uc} translates this coalesced changes into a meaningful sequence of actions. However, diff --git a/auditbeat/module/file_integrity/config.go b/auditbeat/module/file_integrity/config.go index e431e640766..b68cbb53309 100644 --- a/auditbeat/module/file_integrity/config.go +++ b/auditbeat/module/file_integrity/config.go @@ -18,10 +18,12 @@ package file_integrity import ( + "errors" "fmt" "math" "path/filepath" "regexp" + "runtime" "sort" "strings" @@ -72,6 +74,19 @@ const ( XXH64 HashType = "xxh64" ) +type Backend string + +const ( + BackendFSNotify Backend = "fsnotify" + BackendEBPF Backend = "ebpf" + BackendKProbes Backend = "kprobes" +) + +func (b *Backend) Unpack(v string) error { + *b = Backend(v) + return nil +} + // Config contains the configuration parameters for the file integrity // metricset. type Config struct { @@ -86,6 +101,7 @@ type Config struct { Recursive bool `config:"recursive"` // Recursive enables recursive monitoring of directories. ExcludeFiles []match.Matcher `config:"exclude_files"` IncludeFiles []match.Matcher `config:"include_files"` + ForceBackend Backend `config:"force_backend"` } // Validate validates the config data and return an error explaining all the @@ -160,6 +176,19 @@ nextHash: if err != nil { errs = append(errs, fmt.Errorf("invalid scan_rate_per_sec value: %w", err)) } + + if len(c.ForceBackend) > 0 { + if runtime.GOOS != "linux" { + errs = append(errs, errors.New("force_backend can only be specified on linux")) + } + + switch c.ForceBackend { + case BackendEBPF, BackendKProbes, BackendFSNotify: + default: + errs = append(errs, errors.New("force_backend can only be 'ebpf', 'kprobes' or 'fsnotify'")) + } + } + return errs.Err() } diff --git a/x-pack/auditbeat/auditbeat.reference.yml b/x-pack/auditbeat/auditbeat.reference.yml index 45d1c4af851..44b39ee8f63 100644 --- a/x-pack/auditbeat/auditbeat.reference.yml +++ b/x-pack/auditbeat/auditbeat.reference.yml @@ -92,6 +92,10 @@ auditbeat.modules: # Auditbeat will ignore files unless they match a pattern. #include_files: #- '/\.ssh($|/)' + # Select the backend which will be used to source events. + # Valid values: ebpf, kprobes, fsnotify. + # Default: fsnotify. + force_backend: fsnotify # Scan over the configured file paths at startup and send events for new or # modified files since the last time Auditbeat was running. From bd6bcfb8ae5c57ead67e16719db79c01dcc51019 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 31 Jan 2024 04:24:48 +0200 Subject: [PATCH 19/39] ci: add necessary volume mounts for kprobes backend in auditbeat docker-compose.yml --- auditbeat/docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/auditbeat/docker-compose.yml b/auditbeat/docker-compose.yml index adf33888988..0edea1c060d 100644 --- a/auditbeat/docker-compose.yml +++ b/auditbeat/docker-compose.yml @@ -14,6 +14,7 @@ services: - KIBANA_PORT=5601 volumes: - ${PWD}/..:/go/src/github.com/elastic/beats/ + - /sys:/sys command: make privileged: true pid: host From 651b2f78e45bd4fa976ceed096fc1ea2452f1ca8 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 31 Jan 2024 04:47:30 +0200 Subject: [PATCH 20/39] feat: add the instantiation of file integrity module with kprobes backend --- .../module/file_integrity/eventreader_fsnotify.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/auditbeat/module/file_integrity/eventreader_fsnotify.go b/auditbeat/module/file_integrity/eventreader_fsnotify.go index b49bb7b7905..fa3015f76e1 100644 --- a/auditbeat/module/file_integrity/eventreader_fsnotify.go +++ b/auditbeat/module/file_integrity/eventreader_fsnotify.go @@ -23,6 +23,7 @@ import ( "errors" "fmt" "path/filepath" + "runtime" "syscall" "time" @@ -43,6 +44,18 @@ type reader struct { // NewEventReader creates a new EventProducer backed by fsnotify. func NewEventReader(c Config) (EventProducer, error) { + + if runtime.GOOS == "linux" { + switch c.ForceBackend { + case BackendKProbes: + return &kProbesReader{ + config: c, + log: logp.NewLogger(moduleName), + parsers: FileParsers(c), + }, nil + } + } + return &reader{ config: c, log: logp.NewLogger(moduleName), @@ -109,7 +122,7 @@ func (r *reader) enqueueEvents(done <-chan struct{}) (events []*Event) { for { ev := r.nextEvent(done) if ev == nil { - return + return events } events = append(events, ev) } From d39b22f6970d85635662284b826dc1e9d0a5ab0d Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 31 Jan 2024 05:08:59 +0200 Subject: [PATCH 21/39] doc: update CHANGELOG.next.asciidoc --- CHANGELOG.next.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.next.asciidoc b/CHANGELOG.next.asciidoc index 8203c6d8f0b..c6b00d4d2df 100644 --- a/CHANGELOG.next.asciidoc +++ b/CHANGELOG.next.asciidoc @@ -14,6 +14,7 @@ https://github.com/elastic/beats/compare/v8.8.1\...main[Check the HEAD diff] *Auditbeat* +- Add opt-in `KProbes` backend for file_integrity module. {pull}37796[37796] *Filebeat* From 1509a1a64064889de41a94d456cabc39b2aef5f9 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 31 Jan 2024 06:29:32 +0200 Subject: [PATCH 22/39] fix: address compilation issues for non-linux oses --- .../file_integrity/eventreader_fsnotify.go | 32 +++----------- .../file_integrity/eventreader_linux.go | 44 +++++++++++++++++++ .../file_integrity/eventreader_other.go | 33 ++++++++++++++ 3 files changed, 82 insertions(+), 27 deletions(-) create mode 100644 auditbeat/module/file_integrity/eventreader_linux.go create mode 100644 auditbeat/module/file_integrity/eventreader_other.go diff --git a/auditbeat/module/file_integrity/eventreader_fsnotify.go b/auditbeat/module/file_integrity/eventreader_fsnotify.go index fa3015f76e1..6d10ebbd08f 100644 --- a/auditbeat/module/file_integrity/eventreader_fsnotify.go +++ b/auditbeat/module/file_integrity/eventreader_fsnotify.go @@ -23,7 +23,6 @@ import ( "errors" "fmt" "path/filepath" - "runtime" "syscall" "time" @@ -33,7 +32,7 @@ import ( "github.com/elastic/elastic-agent-libs/logp" ) -type reader struct { +type fsNotifyReader struct { watcher monitor.Watcher config Config eventC chan Event @@ -42,28 +41,7 @@ type reader struct { parsers []FileParser } -// NewEventReader creates a new EventProducer backed by fsnotify. -func NewEventReader(c Config) (EventProducer, error) { - - if runtime.GOOS == "linux" { - switch c.ForceBackend { - case BackendKProbes: - return &kProbesReader{ - config: c, - log: logp.NewLogger(moduleName), - parsers: FileParsers(c), - }, nil - } - } - - return &reader{ - config: c, - log: logp.NewLogger(moduleName), - parsers: FileParsers(c), - }, nil -} - -func (r *reader) Start(done <-chan struct{}) (<-chan Event, error) { +func (r *fsNotifyReader) Start(done <-chan struct{}) (<-chan Event, error) { watcher, err := monitor.New(r.config.Recursive, r.config.IsExcludedPath) if err != nil { return nil, err @@ -118,7 +96,7 @@ func (r *reader) Start(done <-chan struct{}) (<-chan Event, error) { return r.eventC, nil } -func (r *reader) enqueueEvents(done <-chan struct{}) (events []*Event) { +func (r *fsNotifyReader) enqueueEvents(done <-chan struct{}) (events []*Event) { for { ev := r.nextEvent(done) if ev == nil { @@ -128,7 +106,7 @@ func (r *reader) enqueueEvents(done <-chan struct{}) (events []*Event) { } } -func (r *reader) consumeEvents(done <-chan struct{}) { +func (r *fsNotifyReader) consumeEvents(done <-chan struct{}) { defer close(r.eventC) defer r.watcher.Close() @@ -142,7 +120,7 @@ func (r *reader) consumeEvents(done <-chan struct{}) { } } -func (r *reader) nextEvent(done <-chan struct{}) *Event { +func (r *fsNotifyReader) nextEvent(done <-chan struct{}) *Event { for { select { case <-done: diff --git a/auditbeat/module/file_integrity/eventreader_linux.go b/auditbeat/module/file_integrity/eventreader_linux.go new file mode 100644 index 00000000000..d56aafe2a72 --- /dev/null +++ b/auditbeat/module/file_integrity/eventreader_linux.go @@ -0,0 +1,44 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//go:build linux + +package file_integrity + +import ( + "github.com/elastic/elastic-agent-libs/logp" +) + +// NewEventReader creates a new EventProducer backed by fsnotify. +func NewEventReader(c Config) (EventProducer, error) { + switch c.ForceBackend { + case BackendKProbes: + return &kProbesReader{ + config: c, + log: logp.NewLogger(moduleName), + parsers: FileParsers(c), + }, nil + case BackendFSNotify: + fallthrough + default: + return &fsNotifyReader{ + config: c, + log: logp.NewLogger(moduleName), + parsers: FileParsers(c), + }, nil + } +} diff --git a/auditbeat/module/file_integrity/eventreader_other.go b/auditbeat/module/file_integrity/eventreader_other.go new file mode 100644 index 00000000000..577e38955c1 --- /dev/null +++ b/auditbeat/module/file_integrity/eventreader_other.go @@ -0,0 +1,33 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//go:build freebsd || openbsd || netbsd || windows + +package file_integrity + +import ( + "github.com/elastic/elastic-agent-libs/logp" +) + +// NewEventReader creates a new EventProducer backed by fsnotify. +func NewEventReader(c Config) (EventProducer, error) { + return &fsNotifyReader{ + config: c, + log: logp.NewLogger(moduleName), + parsers: FileParsers(c), + }, nil +} From 0469332a36925bcee056dd407569d372a2fb6de2 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 31 Jan 2024 06:30:00 +0200 Subject: [PATCH 23/39] fix: correct folder permission for path traverser unit-test --- auditbeat/module/file_integrity/kprobes/path_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/auditbeat/module/file_integrity/kprobes/path_test.go b/auditbeat/module/file_integrity/kprobes/path_test.go index 69bdf17dbe8..d1b397cd17f 100644 --- a/auditbeat/module/file_integrity/kprobes/path_test.go +++ b/auditbeat/module/file_integrity/kprobes/path_test.go @@ -96,7 +96,7 @@ func (p *pathTestSuite) TestRecursiveWalkAsync() { createdPathsOrder = append(createdPathsOrder, tmpDir) testDir := filepath.Join(tmpDir, "test_dir") - err = os.Mkdir(testDir, 0o644) + err = os.Mkdir(testDir, 0o744) p.Require().NoError(err) createdPathsWithDepth[testDir] = 2 createdPathsOrder = append(createdPathsOrder, testDir) @@ -227,7 +227,7 @@ func (p *pathTestSuite) TestNonRecursiveWalkAsync() { createdPathsOrder = append(createdPathsOrder, tmpDir) testDir := filepath.Join(tmpDir, "test_dir") - err = os.Mkdir(testDir, 0o644) + err = os.Mkdir(testDir, 0o744) p.Require().NoError(err) testDirTestFile := filepath.Join(tmpDir, "test_dir", "test_file") @@ -404,7 +404,7 @@ func (p *pathTestSuite) TestRecursiveAdd() { createdPathsOrder = append(createdPathsOrder, tmpDir) testDir := filepath.Join(tmpDir, "test_dir") - err = os.Mkdir(testDir, 0o644) + err = os.Mkdir(testDir, 0o744) p.Require().NoError(err) createdPathsWithDepth[testDir] = 1 createdPathsOrder = append(createdPathsOrder, testDir) @@ -514,7 +514,7 @@ func (p *pathTestSuite) TestNonRecursiveAdd() { createdPathsOrder = append(createdPathsOrder, tmpDir) testDir := filepath.Join(tmpDir, "test_dir") - err = os.Mkdir(testDir, 0o644) + err = os.Mkdir(testDir, 0o744) p.Require().NoError(err) createdPathsWithDepth[testDir] = 1 createdPathsOrder = append(createdPathsOrder, testDir) @@ -638,7 +638,7 @@ func (p *pathTestSuite) TestStatErrAtWalk() { }() testDir := filepath.Join(tmpDir, "test_dir") - err = os.Mkdir(testDir, 0o644) + err = os.Mkdir(testDir, 0o744) p.Require().NoError(err) testDirTestFile := filepath.Join(tmpDir, "test_dir", "test_file") From 6308e8b81af8ecace93d993636d5bf1591c3b332 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 31 Jan 2024 08:43:40 +0200 Subject: [PATCH 24/39] fix: build kprobe package and unit-tests only for linux --- auditbeat/module/file_integrity/kprobes/errors.go | 2 ++ auditbeat/module/file_integrity/kprobes/events.go | 2 ++ auditbeat/module/file_integrity/kprobes/events_cache.go | 2 ++ auditbeat/module/file_integrity/kprobes/events_cache_entry.go | 2 ++ auditbeat/module/file_integrity/kprobes/events_cache_test.go | 2 ++ auditbeat/module/file_integrity/kprobes/events_process.go | 2 ++ auditbeat/module/file_integrity/kprobes/events_process_test.go | 2 ++ auditbeat/module/file_integrity/kprobes/events_test.go | 2 ++ auditbeat/module/file_integrity/kprobes/events_verifier.go | 2 ++ auditbeat/module/file_integrity/kprobes/events_verifier_test.go | 2 ++ auditbeat/module/file_integrity/kprobes/executor.go | 2 ++ auditbeat/module/file_integrity/kprobes/executor_test.go | 2 ++ auditbeat/module/file_integrity/kprobes/kallsyms_test.go | 2 ++ auditbeat/module/file_integrity/kprobes/monitor.go | 2 ++ auditbeat/module/file_integrity/kprobes/monitor_test.go | 2 ++ auditbeat/module/file_integrity/kprobes/path.go | 2 ++ auditbeat/module/file_integrity/kprobes/path_inotify.go | 2 ++ auditbeat/module/file_integrity/kprobes/path_inotify_test.go | 2 ++ auditbeat/module/file_integrity/kprobes/path_mountpoint.go | 2 ++ auditbeat/module/file_integrity/kprobes/path_mountpoint_test.go | 2 ++ auditbeat/module/file_integrity/kprobes/path_test.go | 2 ++ auditbeat/module/file_integrity/kprobes/perf_channel.go | 2 ++ auditbeat/module/file_integrity/kprobes/perf_channel_test.go | 2 ++ auditbeat/module/file_integrity/kprobes/probes.go | 2 ++ auditbeat/module/file_integrity/kprobes/probes_fsnotify.go | 2 ++ .../module/file_integrity/kprobes/probes_fsnotify_nameremove.go | 2 ++ .../file_integrity/kprobes/probes_fsnotify_nameremove_test.go | 2 ++ .../module/file_integrity/kprobes/probes_fsnotify_parent.go | 2 ++ .../file_integrity/kprobes/probes_fsnotify_parent_test.go | 2 ++ auditbeat/module/file_integrity/kprobes/probes_fsnotify_test.go | 2 ++ auditbeat/module/file_integrity/kprobes/probes_vfs_getattr.go | 2 ++ .../module/file_integrity/kprobes/probes_vfs_getattr_test.go | 2 ++ 32 files changed, 64 insertions(+) diff --git a/auditbeat/module/file_integrity/kprobes/errors.go b/auditbeat/module/file_integrity/kprobes/errors.go index 6199a6db986..f3da8878b1b 100644 --- a/auditbeat/module/file_integrity/kprobes/errors.go +++ b/auditbeat/module/file_integrity/kprobes/errors.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import "errors" diff --git a/auditbeat/module/file_integrity/kprobes/events.go b/auditbeat/module/file_integrity/kprobes/events.go index 018f392b717..2ab2b3e1bdb 100644 --- a/auditbeat/module/file_integrity/kprobes/events.go +++ b/auditbeat/module/file_integrity/kprobes/events.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( diff --git a/auditbeat/module/file_integrity/kprobes/events_cache.go b/auditbeat/module/file_integrity/kprobes/events_cache.go index 84f5018da98..e77cd2c6829 100644 --- a/auditbeat/module/file_integrity/kprobes/events_cache.go +++ b/auditbeat/module/file_integrity/kprobes/events_cache.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( diff --git a/auditbeat/module/file_integrity/kprobes/events_cache_entry.go b/auditbeat/module/file_integrity/kprobes/events_cache_entry.go index 0c8c3e528a3..b44b4fe5c41 100644 --- a/auditbeat/module/file_integrity/kprobes/events_cache_entry.go +++ b/auditbeat/module/file_integrity/kprobes/events_cache_entry.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import "strings" diff --git a/auditbeat/module/file_integrity/kprobes/events_cache_test.go b/auditbeat/module/file_integrity/kprobes/events_cache_test.go index 7c83f964ff1..00f7afb846b 100644 --- a/auditbeat/module/file_integrity/kprobes/events_cache_test.go +++ b/auditbeat/module/file_integrity/kprobes/events_cache_test.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( diff --git a/auditbeat/module/file_integrity/kprobes/events_process.go b/auditbeat/module/file_integrity/kprobes/events_process.go index 32691547db2..d0ba1619647 100644 --- a/auditbeat/module/file_integrity/kprobes/events_process.go +++ b/auditbeat/module/file_integrity/kprobes/events_process.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( diff --git a/auditbeat/module/file_integrity/kprobes/events_process_test.go b/auditbeat/module/file_integrity/kprobes/events_process_test.go index e5aa0d5a914..1d0b44b2622 100644 --- a/auditbeat/module/file_integrity/kprobes/events_process_test.go +++ b/auditbeat/module/file_integrity/kprobes/events_process_test.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( diff --git a/auditbeat/module/file_integrity/kprobes/events_test.go b/auditbeat/module/file_integrity/kprobes/events_test.go index a898aecd395..b39e0621ab5 100644 --- a/auditbeat/module/file_integrity/kprobes/events_test.go +++ b/auditbeat/module/file_integrity/kprobes/events_test.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( diff --git a/auditbeat/module/file_integrity/kprobes/events_verifier.go b/auditbeat/module/file_integrity/kprobes/events_verifier.go index 12f8e33c76f..a1870736c88 100644 --- a/auditbeat/module/file_integrity/kprobes/events_verifier.go +++ b/auditbeat/module/file_integrity/kprobes/events_verifier.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( diff --git a/auditbeat/module/file_integrity/kprobes/events_verifier_test.go b/auditbeat/module/file_integrity/kprobes/events_verifier_test.go index bf9647397f1..4daf1cb9f90 100644 --- a/auditbeat/module/file_integrity/kprobes/events_verifier_test.go +++ b/auditbeat/module/file_integrity/kprobes/events_verifier_test.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( diff --git a/auditbeat/module/file_integrity/kprobes/executor.go b/auditbeat/module/file_integrity/kprobes/executor.go index c026644569e..d13b517aca8 100644 --- a/auditbeat/module/file_integrity/kprobes/executor.go +++ b/auditbeat/module/file_integrity/kprobes/executor.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( diff --git a/auditbeat/module/file_integrity/kprobes/executor_test.go b/auditbeat/module/file_integrity/kprobes/executor_test.go index 3083ccbb026..7c899074640 100644 --- a/auditbeat/module/file_integrity/kprobes/executor_test.go +++ b/auditbeat/module/file_integrity/kprobes/executor_test.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( diff --git a/auditbeat/module/file_integrity/kprobes/kallsyms_test.go b/auditbeat/module/file_integrity/kprobes/kallsyms_test.go index d60757afc0f..22dcb59e5f4 100644 --- a/auditbeat/module/file_integrity/kprobes/kallsyms_test.go +++ b/auditbeat/module/file_integrity/kprobes/kallsyms_test.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( diff --git a/auditbeat/module/file_integrity/kprobes/monitor.go b/auditbeat/module/file_integrity/kprobes/monitor.go index 7667baad7db..089e246598c 100644 --- a/auditbeat/module/file_integrity/kprobes/monitor.go +++ b/auditbeat/module/file_integrity/kprobes/monitor.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( diff --git a/auditbeat/module/file_integrity/kprobes/monitor_test.go b/auditbeat/module/file_integrity/kprobes/monitor_test.go index d425284a42d..a8494d0bbf6 100644 --- a/auditbeat/module/file_integrity/kprobes/monitor_test.go +++ b/auditbeat/module/file_integrity/kprobes/monitor_test.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( diff --git a/auditbeat/module/file_integrity/kprobes/path.go b/auditbeat/module/file_integrity/kprobes/path.go index 1253349c9a4..229603570c9 100644 --- a/auditbeat/module/file_integrity/kprobes/path.go +++ b/auditbeat/module/file_integrity/kprobes/path.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( diff --git a/auditbeat/module/file_integrity/kprobes/path_inotify.go b/auditbeat/module/file_integrity/kprobes/path_inotify.go index 3e4185455f5..05d483837f4 100644 --- a/auditbeat/module/file_integrity/kprobes/path_inotify.go +++ b/auditbeat/module/file_integrity/kprobes/path_inotify.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( diff --git a/auditbeat/module/file_integrity/kprobes/path_inotify_test.go b/auditbeat/module/file_integrity/kprobes/path_inotify_test.go index fab167c73d6..a5760765d9b 100644 --- a/auditbeat/module/file_integrity/kprobes/path_inotify_test.go +++ b/auditbeat/module/file_integrity/kprobes/path_inotify_test.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( diff --git a/auditbeat/module/file_integrity/kprobes/path_mountpoint.go b/auditbeat/module/file_integrity/kprobes/path_mountpoint.go index 33ccebca65d..dc8bfcdde99 100644 --- a/auditbeat/module/file_integrity/kprobes/path_mountpoint.go +++ b/auditbeat/module/file_integrity/kprobes/path_mountpoint.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( diff --git a/auditbeat/module/file_integrity/kprobes/path_mountpoint_test.go b/auditbeat/module/file_integrity/kprobes/path_mountpoint_test.go index 314c4a11630..99389d07576 100644 --- a/auditbeat/module/file_integrity/kprobes/path_mountpoint_test.go +++ b/auditbeat/module/file_integrity/kprobes/path_mountpoint_test.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( diff --git a/auditbeat/module/file_integrity/kprobes/path_test.go b/auditbeat/module/file_integrity/kprobes/path_test.go index d1b397cd17f..ee3871eeab8 100644 --- a/auditbeat/module/file_integrity/kprobes/path_test.go +++ b/auditbeat/module/file_integrity/kprobes/path_test.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( diff --git a/auditbeat/module/file_integrity/kprobes/perf_channel.go b/auditbeat/module/file_integrity/kprobes/perf_channel.go index 46c68f9f788..a76fe3f2a63 100644 --- a/auditbeat/module/file_integrity/kprobes/perf_channel.go +++ b/auditbeat/module/file_integrity/kprobes/perf_channel.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( diff --git a/auditbeat/module/file_integrity/kprobes/perf_channel_test.go b/auditbeat/module/file_integrity/kprobes/perf_channel_test.go index 0d1a3e0d3df..810bd59bcff 100644 --- a/auditbeat/module/file_integrity/kprobes/perf_channel_test.go +++ b/auditbeat/module/file_integrity/kprobes/perf_channel_test.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import "github.com/stretchr/testify/mock" diff --git a/auditbeat/module/file_integrity/kprobes/probes.go b/auditbeat/module/file_integrity/kprobes/probes.go index 431d9fe2e3a..836dff04cdf 100644 --- a/auditbeat/module/file_integrity/kprobes/probes.go +++ b/auditbeat/module/file_integrity/kprobes/probes.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( diff --git a/auditbeat/module/file_integrity/kprobes/probes_fsnotify.go b/auditbeat/module/file_integrity/kprobes/probes_fsnotify.go index 5badf59f4eb..f840e891a96 100644 --- a/auditbeat/module/file_integrity/kprobes/probes_fsnotify.go +++ b/auditbeat/module/file_integrity/kprobes/probes_fsnotify.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( diff --git a/auditbeat/module/file_integrity/kprobes/probes_fsnotify_nameremove.go b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_nameremove.go index 9ecad77d8f7..3409d855864 100644 --- a/auditbeat/module/file_integrity/kprobes/probes_fsnotify_nameremove.go +++ b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_nameremove.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( diff --git a/auditbeat/module/file_integrity/kprobes/probes_fsnotify_nameremove_test.go b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_nameremove_test.go index aeb047fe6b0..17e7b3d9f49 100644 --- a/auditbeat/module/file_integrity/kprobes/probes_fsnotify_nameremove_test.go +++ b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_nameremove_test.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( diff --git a/auditbeat/module/file_integrity/kprobes/probes_fsnotify_parent.go b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_parent.go index f0936ed81af..505d40f6b08 100644 --- a/auditbeat/module/file_integrity/kprobes/probes_fsnotify_parent.go +++ b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_parent.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( diff --git a/auditbeat/module/file_integrity/kprobes/probes_fsnotify_parent_test.go b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_parent_test.go index 99c0c32957d..5198cb673cd 100644 --- a/auditbeat/module/file_integrity/kprobes/probes_fsnotify_parent_test.go +++ b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_parent_test.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( diff --git a/auditbeat/module/file_integrity/kprobes/probes_fsnotify_test.go b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_test.go index 18378b0b925..ba02cbf4638 100644 --- a/auditbeat/module/file_integrity/kprobes/probes_fsnotify_test.go +++ b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_test.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( diff --git a/auditbeat/module/file_integrity/kprobes/probes_vfs_getattr.go b/auditbeat/module/file_integrity/kprobes/probes_vfs_getattr.go index d5a1a8a7d75..e115eba4565 100644 --- a/auditbeat/module/file_integrity/kprobes/probes_vfs_getattr.go +++ b/auditbeat/module/file_integrity/kprobes/probes_vfs_getattr.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( diff --git a/auditbeat/module/file_integrity/kprobes/probes_vfs_getattr_test.go b/auditbeat/module/file_integrity/kprobes/probes_vfs_getattr_test.go index 215893e81b7..3b926599316 100644 --- a/auditbeat/module/file_integrity/kprobes/probes_vfs_getattr_test.go +++ b/auditbeat/module/file_integrity/kprobes/probes_vfs_getattr_test.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build linux + package kprobes import ( From c52743b8b1530285e3fee41987da6b39477207ca Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 31 Jan 2024 18:34:34 +0200 Subject: [PATCH 25/39] ci: extend test_file_integrity.py to test kprobes backend of file integrity module --- auditbeat/tests/system/test_file_integrity.py | 215 ++++++++++++++---- 1 file changed, 177 insertions(+), 38 deletions(-) diff --git a/auditbeat/tests/system/test_file_integrity.py b/auditbeat/tests/system/test_file_integrity.py index 280d2916a55..252a372fa14 100644 --- a/auditbeat/tests/system/test_file_integrity.py +++ b/auditbeat/tests/system/test_file_integrity.py @@ -1,9 +1,43 @@ +import os import time import unittest import platform from auditbeat import * +def is_root(): + if 'geteuid' not in dir(os): + return False + euid = os.geteuid() + return euid == 0 + + +def is_version_below(version, target): + t = list(map(int, target.split('.'))) + v = list(map(int, version.split('.'))) + v += [0] * (len(t) - len(v)) + for i in range(len(t)): + if v[i] != t[i]: + return v[i] < t[i] + return False + + +# Require Linux greater or equal than 3.10.0 and arm64/amd64 arch +def is_platform_supported(): + p = platform.platform().split('-') + if p[0] != 'Linux': + return False + if is_version_below(p[1], '3.10.0'): + return False + return {'aarch64', 'arm64', 'x86_64', 'amd64'}.intersection(p) + + +if platform.system() == 'Linux': + fim_backends = ["fsnotify", "kprobes"] +else: + fim_backends = ["fsnotify"] + + # Escapes a path to match what's printed in the logs def escape_path(path): return path.replace('\\', '\\\\') @@ -52,19 +86,27 @@ class Test(BaseTest): def wait_output(self, min_events): self.wait_until(lambda: wrap_except(lambda: len(self.read_output()) >= min_events)) - # wait for the number of lines in the file to stay constant for a second + # wait for the number of lines in the file to stay constant for 10 seconds prev_lines = -1 while True: num_lines = self.output_lines() if prev_lines < num_lines: prev_lines = num_lines - time.sleep(1) + time.sleep(10) else: break - @unittest.skipIf(os.getenv("CI") is not None and platform.system() == 'Darwin', - 'Flaky test: https://github.com/elastic/beats/issues/24678') - def test_non_recursive(self): + def wait_startup(self, backend, dir): + if backend == "kprobes": + self.wait_log_contains("Started kprobes watcher", max_timeout=30, ignore_case=True) + else: + # wait until the directories to watch are printed in the logs + # this happens when the file_integrity module starts. + # Case must be ignored under windows as capitalisation of paths + # may differ + self.wait_log_contains(escape_path(dir), max_timeout=30, ignore_case=True) + + def _test_non_recursive(self, backend): """ file_integrity monitors watched directories (non recursive). """ @@ -73,22 +115,21 @@ def test_non_recursive(self): self.temp_dir("auditbeat_test")] with PathCleanup(dirs): + extras = { + "paths": dirs, + "scan_at_start": False + } + if platform.system() == "Linux": + extras["force_backend"] = backend + self.render_config_template( modules=[{ "name": "file_integrity", - "extras": { - "paths": dirs, - "scan_at_start": False - } + "extras": extras }], ) proc = self.start_beat() - - # wait until the directories to watch are printed in the logs - # this happens when the file_integrity module starts. - # Case must be ignored under windows as capitalisation of paths - # may differ - self.wait_log_contains(escape_path(dirs[0]), max_timeout=30, ignore_case=True) + self.wait_startup(backend, dirs[0]) file1 = os.path.join(dirs[0], 'file.txt') self.create_file(file1, "hello world!") @@ -109,10 +150,12 @@ def test_non_recursive(self): # log entries are JSON formatted, this value shows up as an escaped json string. self.wait_log_contains("\\\"deleted\\\"") - self.wait_log_contains("\"path\":\"{0}\"".format(escape_path(subdir)), ignore_case=True) - self.wait_output(3) - self.wait_until(lambda: any( - 'file.path' in obj and obj['file.path'].lower() == subdir.lower() for obj in self.read_output())) + + if backend == "fsnotify" or backend == "kprobes": + self.wait_output(4) + else: + # ebpf backend doesn't catch directory creation + self.wait_output(3) proc.check_kill_and_wait() self.assert_no_logged_warnings() @@ -126,7 +169,8 @@ def test_non_recursive(self): has_file(objs, file1, "430ce34d020724ed75a196dfc2ad67c77772d169") has_file(objs, file2, "d23be250530a24be33069572db67995f21244c51") - has_dir(objs, subdir) + if backend == "fsnotify" or backend == "kprobes": + has_dir(objs, subdir) file_events(objs, file1, ['created', 'deleted']) file_events(objs, file2, ['created']) @@ -134,8 +178,17 @@ def test_non_recursive(self): # assert file inside subdir is not reported assert self.log_contains(file3) is False - @unittest.skipIf(os.getenv("BUILD_ID") is not None, "Skipped as flaky: https://github.com/elastic/beats/issues/7731") - def test_recursive(self): + @unittest.skipIf(os.getenv("CI") is not None and platform.system() == 'Darwin', + 'Flaky test: https://github.com/elastic/beats/issues/24678') + def test_non_recursive_fsnotify(self): + self._test_non_recursive("fsnotify") + + @unittest.skipUnless(is_platform_supported(), "Requires Linux 3.10.0+ and arm64/amd64 arch") + @unittest.skipUnless(is_root(), "Requires root") + def test_non_recursive_kprobes(self): + self._test_non_recursive("kprobes") + + def _test_recursive(self, backend): """ file_integrity monitors watched directories (recursive). """ @@ -143,22 +196,23 @@ def test_recursive(self): dirs = [self.temp_dir("auditbeat_test")] with PathCleanup(dirs): + extras = { + "paths": dirs, + "scan_at_start": False, + "recursive": True + } + + if platform.system() == "Linux": + extras["force_backend"] = backend + self.render_config_template( modules=[{ "name": "file_integrity", - "extras": { - "paths": dirs, - "scan_at_start": False, - "recursive": True - } + "extras": extras }], ) proc = self.start_beat() - - # wait until the directories to watch are printed in the logs - # this happens when the file_integrity module starts - self.wait_log_contains(escape_path(dirs[0]), max_timeout=30, ignore_case=True) - self.wait_log_contains("\"recursive\":true") + self.wait_startup(backend, dirs[0]) # auditbeat_test/subdir/ subdir = os.path.join(dirs[0], "subdir") @@ -174,10 +228,13 @@ def test_recursive(self): file2 = os.path.join(subdir2, "more.txt") self.create_file(file2, "") - self.wait_log_contains("\"path\":\"{0}\"".format(escape_path(file2)), ignore_case=True) - self.wait_output(4) - self.wait_until(lambda: any( - 'file.path' in obj and obj['file.path'].lower() == subdir2.lower() for obj in self.read_output())) + if backend == "fsnotify" or backend == "kprobes": + self.wait_output(4) + self.wait_until(lambda: any( + 'file.path' in obj and obj['file.path'].lower() == subdir2.lower() for obj in self.read_output())) + else: + # ebpf backend doesn't catch directory creation + self.wait_output(2) proc.check_kill_and_wait() self.assert_no_logged_warnings() @@ -191,8 +248,90 @@ def test_recursive(self): has_file(objs, file1, "430ce34d020724ed75a196dfc2ad67c77772d169") has_file(objs, file2, "da39a3ee5e6b4b0d3255bfef95601890afd80709") - has_dir(objs, subdir) - has_dir(objs, subdir2) + if backend == "fsnotify" or backend == "kprobes": + has_dir(objs, subdir) + has_dir(objs, subdir2) file_events(objs, file1, ['created']) file_events(objs, file2, ['created']) + + def test_recursive_fsnotify(self): + self._test_recursive("fsnotify") + + @unittest.skipUnless(is_platform_supported(), "Requires Linux 3.10.0+ and arm64/amd64 arch") + @unittest.skipUnless(is_root(), "Requires root") + def test_recursive_kprobes(self): + self._test_recursive("kprobes") + + def _test_file_modified(self, backend): + """ + file_integrity tests for file modifications (chmod, chown, write, truncate, xattrs). + """ + + dirs = [self.temp_dir("auditbeat_test")] + + with PathCleanup(dirs): + + self.render_config_template( + modules=[{ + "name": "file_integrity", + "extras": { + "paths": dirs, + "scan_at_start": False, + "recursive": False, + "force_backend": backend + } + }], + ) + proc = self.start_beat() + self.wait_startup(backend, dirs[0]) + + # Event 1: file create + f = os.path.join(dirs[0], f'file_{backend}.txt') + self.create_file(f, "hello world!") + + if backend == "fsnotify" or backend == "kprobes": + # FSNotify can't catch the events if operations happens too fast + time.sleep(1) + + # Event 2: chmod + os.chmod(f, 0o777) + + if backend == "fsnotify" or backend == "kprobes": + # FSNotify can't catch the events if operations happens too fast + time.sleep(1) + + with open(f, "w") as fd: + # Event 3: write + fd.write("data") + + if backend == "fsnotify" or backend == "kprobes": + # FSNotify can't catch the events if operations happens too fast + time.sleep(1) + + # Event 4: truncate + fd.truncate(0) + + if backend == "fsnotify" or backend == "kprobes": + # FSNotify can't catch the events if operations happens too fast + time.sleep(1) + + # Wait N events + self.wait_output(4) + + proc.check_kill_and_wait() + self.assert_no_logged_warnings() + + # Ensure all Beater stages are used. + assert self.log_contains("Setup Beat: auditbeat") + assert self.log_contains("auditbeat start running") + assert self.log_contains("auditbeat stopped") + + @unittest.skipIf(platform.system() != 'Linux', 'Non linux, skipping.') + def test_file_modified_fsnotify(self): + self._test_file_modified("fsnotify") + + @unittest.skipUnless(is_platform_supported(), "Requires Linux 3.10.0+ and arm64/amd64 arch") + @unittest.skipUnless(is_root(), "Requires root") + def test_file_modified_kprobes(self): + self._test_file_modified("kprobes") From 6ccd47928ad9822aa1fbd44406ea82acbeaf0d9a Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Thu, 1 Feb 2024 00:22:57 +0200 Subject: [PATCH 26/39] ci: extend TestNew in monitor to include actual file changes --- .../file_integrity/kprobes/monitor_test.go | 89 ++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) diff --git a/auditbeat/module/file_integrity/kprobes/monitor_test.go b/auditbeat/module/file_integrity/kprobes/monitor_test.go index a8494d0bbf6..3b57b6661f3 100644 --- a/auditbeat/module/file_integrity/kprobes/monitor_test.go +++ b/auditbeat/module/file_integrity/kprobes/monitor_test.go @@ -486,11 +486,98 @@ func (p *monitorTestSuite) TestNew() { return } + runtime.LockOSThread() + defer runtime.UnlockOSThread() + m, err := New(true) p.Require().NoError(err) + tmpDir, err := os.MkdirTemp("", "kprobe_bench_test") + p.Require().NoError(err) + defer os.RemoveAll(tmpDir) + + errChan := make(chan error) + cancelChan := make(chan struct{}) + + targetFile := filepath.Join(tmpDir, "file_kprobes.txt") + tid := uint32(unix.Gettid()) + + expectedEvents := []MonitorEvent{ + { + Op: uint32(unix.IN_CREATE), + Path: targetFile, + PID: tid, + }, + { + Op: uint32(unix.IN_MODIFY), + Path: targetFile, + PID: tid, + }, + { + Op: uint32(unix.IN_ATTRIB), + Path: targetFile, + PID: tid, + }, + { + Op: uint32(unix.IN_MODIFY), + Path: targetFile, + PID: tid, + }, + { + Op: uint32(unix.IN_MODIFY), + Path: targetFile, + PID: tid, + }, + { + Op: uint32(unix.IN_MODIFY), + Path: targetFile, + PID: tid, + }, + } + + var seenEvents []MonitorEvent + go func() { + defer close(errChan) + for { + select { + case mErr := <-m.ErrorChannel(): + select { + case errChan <- mErr: + case <-cancelChan: + return + } + case e, ok := <-m.EventChannel(): + if !ok { + select { + case errChan <- errors.New("closed event channel"): + case <-cancelChan: + return + } + } + seenEvents = append(seenEvents, e) + continue + case <-cancelChan: + return + } + } + }() + p.Require().NoError(m.Start()) - p.Require().NoError(m.Close()) + p.Require().NoError(m.Add(tmpDir)) + + p.Require().NoError(os.WriteFile(targetFile, []byte("hello world!"), 0o644)) + p.Require().NoError(os.Chmod(targetFile, 0o777)) + p.Require().NoError(os.WriteFile(targetFile, []byte("data"), 0o644)) + p.Require().NoError(os.Truncate(targetFile, 0)) + + time.Sleep(5 * time.Second) + close(cancelChan) + err = <-errChan + if err != nil { + p.Require().Fail(err.Error()) + } + + p.Require().Equal(expectedEvents, seenEvents) } const kernelURL string = "https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.7.tar.xz" From 82a07be9ce63b6626e3f5f2d061385ff05ae6fc9 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Thu, 1 Feb 2024 02:28:12 +0200 Subject: [PATCH 27/39] ci: mark with nolint prealloc slices that can't be pre-allocated --- auditbeat/module/file_integrity/eventreader_kprobes.go | 2 +- auditbeat/module/file_integrity/kprobes/path_mountpoint.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/auditbeat/module/file_integrity/eventreader_kprobes.go b/auditbeat/module/file_integrity/eventreader_kprobes.go index 6d531a4e2d6..41c8fe0dd31 100644 --- a/auditbeat/module/file_integrity/eventreader_kprobes.go +++ b/auditbeat/module/file_integrity/eventreader_kprobes.go @@ -97,7 +97,7 @@ func (r kProbesReader) Start(done <-chan struct{}) (<-chan Event, error) { } func (r kProbesReader) enqueueEvents(done <-chan struct{}) []*Event { - var events []*Event + var events []*Event //nolint:prealloc //can't be preallocated as the number of events is unknown for { ev := r.nextEvent(done) if ev == nil { diff --git a/auditbeat/module/file_integrity/kprobes/path_mountpoint.go b/auditbeat/module/file_integrity/kprobes/path_mountpoint.go index dc8bfcdde99..b15eb184c22 100644 --- a/auditbeat/module/file_integrity/kprobes/path_mountpoint.go +++ b/auditbeat/module/file_integrity/kprobes/path_mountpoint.go @@ -176,7 +176,7 @@ func parseMountInfoLine(line string) *mount { // struct containing information about the mount. func readMountInfo(r io.Reader) (mountPoints, error) { seenMountsByPath := make(map[string]*mount) - var mPoints mountPoints + var mPoints mountPoints //nolint:prealloc //can't be preallocated as the number of lines is unknown before scan scanner := bufio.NewScanner(r) for scanner.Scan() { From 4650e5fe746f60e240cebd486e4fc7358613d8ba Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Fri, 9 Feb 2024 17:19:56 +0200 Subject: [PATCH 28/39] chore: inline defer funcs --- .../module/file_integrity/kprobes/kallsyms.go | 4 +-- .../kprobes/path_inotify_test.go | 13 +++---- .../file_integrity/kprobes/path_mountpoint.go | 4 +-- .../file_integrity/kprobes/path_test.go | 35 ++++++------------- .../module/file_integrity/kprobes/verifier.go | 8 ++--- 5 files changed, 19 insertions(+), 45 deletions(-) diff --git a/auditbeat/module/file_integrity/kprobes/kallsyms.go b/auditbeat/module/file_integrity/kprobes/kallsyms.go index fd146660246..74229a3e5f5 100644 --- a/auditbeat/module/file_integrity/kprobes/kallsyms.go +++ b/auditbeat/module/file_integrity/kprobes/kallsyms.go @@ -44,9 +44,7 @@ func getSymbolInfoRuntime(symbolName string) (runtimeSymbolInfo, error) { return runtimeSymbolInfo{}, err } - defer func() { - _ = kAllSymsFile.Close() - }() + defer kAllSymsFile.Close() return getSymbolInfoFromReader(kAllSymsFile, symbolName) } diff --git a/auditbeat/module/file_integrity/kprobes/path_inotify_test.go b/auditbeat/module/file_integrity/kprobes/path_inotify_test.go index a5760765d9b..db93e683b14 100644 --- a/auditbeat/module/file_integrity/kprobes/path_inotify_test.go +++ b/auditbeat/module/file_integrity/kprobes/path_inotify_test.go @@ -36,9 +36,7 @@ func Test_InotifyWatcher(t *testing.T) { tmpDir, err := os.MkdirTemp("", "kprobe_unit_test") require.NoError(t, err) - defer func() { - _ = os.RemoveAll(tmpDir) - }() + defer os.RemoveAll(tmpDir) watcher, err := newInotifyWatcher() require.NoError(t, err) @@ -57,9 +55,8 @@ func Test_InotifyWatcher(t *testing.T) { tmpDir2, err := os.MkdirTemp("", "kprobe_unit_test") require.NoError(t, err) - defer func() { - _ = os.RemoveAll(tmpDir2) - }() + defer os.RemoveAll(tmpDir2) + added, err = watcher.Add(2, 2, tmpDir2) require.NoError(t, err) require.True(t, added) @@ -98,9 +95,7 @@ func Test_InotifyWatcher_Close_Err(t *testing.T) { tmpDir, err := os.MkdirTemp("", "kprobe_unit_test") require.NoError(t, err) - defer func() { - _ = os.RemoveAll(tmpDir) - }() + defer os.RemoveAll(tmpDir) watcher, err := newInotifyWatcher() require.NoError(t, err) diff --git a/auditbeat/module/file_integrity/kprobes/path_mountpoint.go b/auditbeat/module/file_integrity/kprobes/path_mountpoint.go index b15eb184c22..8223074e1be 100644 --- a/auditbeat/module/file_integrity/kprobes/path_mountpoint.go +++ b/auditbeat/module/file_integrity/kprobes/path_mountpoint.go @@ -213,9 +213,7 @@ func getAllMountPoints() (mountPoints, error) { if err != nil { return nil, err } - defer func() { - _ = file.Close() - }() + defer file.Close() return readMountInfo(file) } diff --git a/auditbeat/module/file_integrity/kprobes/path_test.go b/auditbeat/module/file_integrity/kprobes/path_test.go index ee3871eeab8..cd0bc255535 100644 --- a/auditbeat/module/file_integrity/kprobes/path_test.go +++ b/auditbeat/module/file_integrity/kprobes/path_test.go @@ -91,9 +91,7 @@ func (p *pathTestSuite) TestRecursiveWalkAsync() { createdPathsWithDepth := make(map[string]uint32) tmpDir, err := os.MkdirTemp("", "kprobe_unit_test") p.Require().NoError(err) - defer func() { - _ = os.RemoveAll(tmpDir) - }() + defer os.RemoveAll(tmpDir) createdPathsWithDepth[tmpDir] = 1 createdPathsOrder = append(createdPathsOrder, tmpDir) @@ -195,9 +193,7 @@ func (p *pathTestSuite) TestWalkAsyncTimeoutErr() { tmpDir, err := os.MkdirTemp("", "kprobe_unit_test") p.Require().NoError(err) - defer func() { - _ = os.RemoveAll(tmpDir) - }() + defer os.RemoveAll(tmpDir) ctx := context.Background() pTrav, err := newPathMonitor(ctx, newFixedThreadExecutor(ctx), 0, true) @@ -222,9 +218,8 @@ func (p *pathTestSuite) TestNonRecursiveWalkAsync() { createdPathsWithDepth := make(map[string]uint32) tmpDir, err := os.MkdirTemp("", "kprobe_unit_test") p.Require().NoError(err) - defer func() { - _ = os.RemoveAll(tmpDir) - }() + defer os.RemoveAll(tmpDir) + createdPathsWithDepth[tmpDir] = 1 createdPathsOrder = append(createdPathsOrder, tmpDir) @@ -319,9 +314,7 @@ func (p *pathTestSuite) TestNonRecursiveWalkAsync() { func (p *pathTestSuite) TestAddTraverserContextCancel() { tmpDir, err := os.MkdirTemp("", "kprobe_unit_test") p.Require().NoError(err) - defer func() { - _ = os.RemoveAll(tmpDir) - }() + defer os.RemoveAll(tmpDir) ctx := context.Background() pTrav, err := newPathMonitor(ctx, newFixedThreadExecutor(ctx), 10*time.Second, true) @@ -363,9 +356,7 @@ func (p *pathTestSuite) TestAddTraverserContextCancel() { func (p *pathTestSuite) TestAddTimeout() { tmpDir, err := os.MkdirTemp("", "kprobe_unit_test") p.Require().NoError(err) - defer func() { - _ = os.RemoveAll(tmpDir) - }() + defer os.RemoveAll(tmpDir) ctx := context.Background() pTrav, err := newPathMonitor(ctx, newFixedThreadExecutor(ctx), 5*time.Second, true) @@ -399,9 +390,8 @@ func (p *pathTestSuite) TestRecursiveAdd() { createdPathsWithDepth := make(map[string]uint32) tmpDir, err := os.MkdirTemp("", "kprobe_unit_test") p.Require().NoError(err) - defer func() { - _ = os.RemoveAll(tmpDir) - }() + defer os.RemoveAll(tmpDir) + createdPathsWithDepth[tmpDir] = 0 createdPathsOrder = append(createdPathsOrder, tmpDir) @@ -509,9 +499,8 @@ func (p *pathTestSuite) TestNonRecursiveAdd() { createdPathsWithDepth := make(map[string]uint32) tmpDir, err := os.MkdirTemp("", "kprobe_unit_test") p.Require().NoError(err) - defer func() { - _ = os.RemoveAll(tmpDir) - }() + defer os.RemoveAll(tmpDir) + createdPathsWithDepth[tmpDir] = 0 createdPathsOrder = append(createdPathsOrder, tmpDir) @@ -635,9 +624,7 @@ func (p *pathTestSuite) TestStatErrAtWalk() { tmpDir, err := os.MkdirTemp("", "kprobe_unit_test") p.Require().NoError(err) - defer func() { - _ = os.RemoveAll(tmpDir) - }() + defer os.RemoveAll(tmpDir) testDir := filepath.Join(tmpDir, "test_dir") err = os.Mkdir(testDir, 0o744) diff --git a/auditbeat/module/file_integrity/kprobes/verifier.go b/auditbeat/module/file_integrity/kprobes/verifier.go index a270fc4bfb4..ccf0797c412 100644 --- a/auditbeat/module/file_integrity/kprobes/verifier.go +++ b/auditbeat/module/file_integrity/kprobes/verifier.go @@ -141,9 +141,7 @@ func verify(ctx context.Context, exec executor, probes map[tracing.Probe]tracing return err } - defer func() { - _ = os.RemoveAll(basePath) - }() + defer os.RemoveAll(basePath) verifier, err := newEventsVerifier(basePath) if err != nil { @@ -160,9 +158,7 @@ func verify(ctx context.Context, exec executor, probes map[tracing.Probe]tracing return err } - defer func() { - _ = m.Close() - }() + defer m.Close() // start the monitor if err := m.Start(); err != nil { From 62ea80716b63c37a7ffdf4dde9ac774da835b8c7 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Fri, 9 Feb 2024 17:20:17 +0200 Subject: [PATCH 29/39] fix: return the scanner error if any --- auditbeat/module/file_integrity/kprobes/kallsyms.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/auditbeat/module/file_integrity/kprobes/kallsyms.go b/auditbeat/module/file_integrity/kprobes/kallsyms.go index 74229a3e5f5..88ea033f884 100644 --- a/auditbeat/module/file_integrity/kprobes/kallsyms.go +++ b/auditbeat/module/file_integrity/kprobes/kallsyms.go @@ -89,5 +89,9 @@ func getSymbolInfoFromReader(reader io.Reader, symbolName string) (runtimeSymbol } } + if fileScanner.Err() != nil { + return runtimeSymbolInfo{}, err + } + return runtimeSymbolInfo{}, ErrSymbolNotFound } From f1cff5832f71756fc15d78b2b0abf1422adf26ea Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Fri, 9 Feb 2024 17:25:41 +0200 Subject: [PATCH 30/39] fix: remove redundant runtime os checks for linux --- .../file_integrity/kprobes/events_verifier_test.go | 4 ---- .../module/file_integrity/kprobes/executor_test.go | 5 ----- .../module/file_integrity/kprobes/monitor_test.go | 10 ---------- .../module/file_integrity/kprobes/path_inotify_test.go | 10 ---------- 4 files changed, 29 deletions(-) diff --git a/auditbeat/module/file_integrity/kprobes/events_verifier_test.go b/auditbeat/module/file_integrity/kprobes/events_verifier_test.go index 4daf1cb9f90..5bd4c955b54 100644 --- a/auditbeat/module/file_integrity/kprobes/events_verifier_test.go +++ b/auditbeat/module/file_integrity/kprobes/events_verifier_test.go @@ -21,7 +21,6 @@ package kprobes import ( "os" - "runtime" "testing" "github.com/stretchr/testify/require" @@ -176,9 +175,6 @@ func Test_EventsVerifier(t *testing.T) { } func Test_EventsVerifier_GenerateEvents(t *testing.T) { - if runtime.GOOS != "linux" { - t.Skip("skipping on non-linux") - } tmpDir, err := os.MkdirTemp("", "kprobe_unit_test") require.NoError(t, err) diff --git a/auditbeat/module/file_integrity/kprobes/executor_test.go b/auditbeat/module/file_integrity/kprobes/executor_test.go index 7c899074640..eb9c0395147 100644 --- a/auditbeat/module/file_integrity/kprobes/executor_test.go +++ b/auditbeat/module/file_integrity/kprobes/executor_test.go @@ -22,7 +22,6 @@ package kprobes import ( "context" "errors" - "runtime" "sync" "sync/atomic" "testing" @@ -33,10 +32,6 @@ import ( func Test_executor(t *testing.T) { - if runtime.GOOS != "linux" { - t.Skip("skipping on non-linux") - } - // parent context is cancelled at creation ctx, cancel := context.WithCancel(context.Background()) cancel() diff --git a/auditbeat/module/file_integrity/kprobes/monitor_test.go b/auditbeat/module/file_integrity/kprobes/monitor_test.go index 3b57b6661f3..0c075238975 100644 --- a/auditbeat/module/file_integrity/kprobes/monitor_test.go +++ b/auditbeat/module/file_integrity/kprobes/monitor_test.go @@ -476,11 +476,6 @@ func (p *monitorTestSuite) TestNew() { return } - if runtime.GOOS != "linux" { - p.T().Skip("skipping on non-linux") - return - } - if os.Getuid() != 0 { p.T().Skip("skipping as non-root") return @@ -613,11 +608,6 @@ func BenchmarkMonitor(b *testing.B) { return } - if runtime.GOOS != "linux" { - b.Skip("skipping on non-linux") - return - } - if os.Getuid() != 0 { b.Skip("skipping as non-root") return diff --git a/auditbeat/module/file_integrity/kprobes/path_inotify_test.go b/auditbeat/module/file_integrity/kprobes/path_inotify_test.go index db93e683b14..51608d751ce 100644 --- a/auditbeat/module/file_integrity/kprobes/path_inotify_test.go +++ b/auditbeat/module/file_integrity/kprobes/path_inotify_test.go @@ -22,7 +22,6 @@ package kprobes import ( "os" "path/filepath" - "runtime" "testing" "github.com/stretchr/testify/require" @@ -30,9 +29,6 @@ import ( ) func Test_InotifyWatcher(t *testing.T) { - if runtime.GOOS != "linux" { - t.Skip("skipping on non-linux") - } tmpDir, err := os.MkdirTemp("", "kprobe_unit_test") require.NoError(t, err) @@ -68,9 +64,6 @@ func Test_InotifyWatcher(t *testing.T) { } func Test_InotifyWatcher_Add_Err(t *testing.T) { - if runtime.GOOS != "linux" { - t.Skip("skipping on non-linux") - } watcher, err := newInotifyWatcher() require.NoError(t, err) @@ -89,9 +82,6 @@ func Test_InotifyWatcher_Add_Err(t *testing.T) { } func Test_InotifyWatcher_Close_Err(t *testing.T) { - if runtime.GOOS != "linux" { - t.Skip("skipping on non-linux") - } tmpDir, err := os.MkdirTemp("", "kprobe_unit_test") require.NoError(t, err) From 53505965767a1b40e864e15b09fcb88c272747a9 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Fri, 9 Feb 2024 18:06:33 +0200 Subject: [PATCH 31/39] doc: comment that dEntryCache is not thread-safe --- auditbeat/module/file_integrity/kprobes/events_cache.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/auditbeat/module/file_integrity/kprobes/events_cache.go b/auditbeat/module/file_integrity/kprobes/events_cache.go index e77cd2c6829..e53b7ead75e 100644 --- a/auditbeat/module/file_integrity/kprobes/events_cache.go +++ b/auditbeat/module/file_integrity/kprobes/events_cache.go @@ -26,6 +26,9 @@ import ( type dEntriesIndex map[dKey]*dEntry type dEntriesMoveIndex map[uint64]*dEntry +// dEntryCache is a cache of directory entries (dEntries) that exposes appropriate methods to add, get, remove and +// handle move operations. Note that dEntryCache is designed to be utilised by a single goroutine at a time and thus +// is not thread safe. type dEntryCache struct { index dEntriesIndex moveCache dEntriesMoveIndex From fe6453a64f5677c52c36a3466a4a9a2946236de3 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Fri, 9 Feb 2024 18:08:44 +0200 Subject: [PATCH 32/39] fix: set the appropriate verbosity of errors of watcher --- auditbeat/module/file_integrity/eventreader_kprobes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auditbeat/module/file_integrity/eventreader_kprobes.go b/auditbeat/module/file_integrity/eventreader_kprobes.go index 41c8fe0dd31..7cddd7f60cd 100644 --- a/auditbeat/module/file_integrity/eventreader_kprobes.go +++ b/auditbeat/module/file_integrity/eventreader_kprobes.go @@ -158,7 +158,7 @@ func (r kProbesReader) nextEvent(done <-chan struct{}) *Event { case err := <-r.watcher.ErrorChannel(): if err != nil { - r.log.Warnw("kprobes watcher error", "error", err) + r.log.Errorw("kprobes watcher error", "error", err) } } } From da8427768ad28c898bcc20a9a2d642409cf0ddb0 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Fri, 9 Feb 2024 18:30:10 +0200 Subject: [PATCH 33/39] fix: check for scanner.Err and return err from parsing mountinfo lines --- .../file_integrity/kprobes/path_mountpoint.go | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/auditbeat/module/file_integrity/kprobes/path_mountpoint.go b/auditbeat/module/file_integrity/kprobes/path_mountpoint.go index 8223074e1be..6029061c83e 100644 --- a/auditbeat/module/file_integrity/kprobes/path_mountpoint.go +++ b/auditbeat/module/file_integrity/kprobes/path_mountpoint.go @@ -135,10 +135,10 @@ func unescapeString(str string) string { // [n+3] super options // // For more details, see https://www.kernel.org/doc/Documentation/filesystems/proc.txt -func parseMountInfoLine(line string) *mount { +func parseMountInfoLine(line string) (*mount, error) { fields := strings.Split(line, " ") if len(fields) < 10 { - return nil + return nil, nil } // Count the optional fields. In case new fields are appended later, @@ -147,18 +147,18 @@ func parseMountInfoLine(line string) *mount { for fields[n] != "-" { n++ if n >= len(fields) { - return nil + return nil, nil } } if n+3 >= len(fields) { - return nil + return nil, nil } mnt := &mount{} var err error mnt.DeviceMajor, mnt.DeviceMinor, err = newDeviceMajorMinorFromString(fields[2]) if err != nil { - return nil + return nil, err } mnt.Subtree = unescapeString(fields[3]) mnt.Path = unescapeString(fields[4]) @@ -168,7 +168,7 @@ func parseMountInfoLine(line string) *mount { } } mnt.FilesystemType = unescapeString(fields[n+1]) - return mnt + return mnt, nil } // readMountInfo reads mount information from the given input reader and returns @@ -181,7 +181,11 @@ func readMountInfo(r io.Reader) (mountPoints, error) { scanner := bufio.NewScanner(r) for scanner.Scan() { line := scanner.Text() - mnt := parseMountInfoLine(line) + mnt, err := parseMountInfoLine(line) + if err != nil { + return nil, err + } + if mnt == nil { continue } @@ -199,6 +203,11 @@ func readMountInfo(r io.Reader) (mountPoints, error) { // mountpoints are listed in mount order. seenMountsByPath[mnt.Path] = mnt } + + if err := scanner.Err(); err != nil { + return nil, err + } + sort.Sort(mPoints) return mPoints, nil From c4d2edbd4fc15b743ed93deb847ead15c91b09a1 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 14 Feb 2024 00:07:24 +0200 Subject: [PATCH 34/39] fix: remove redundant fim_backends list from test_file_integrity.py --- auditbeat/tests/system/test_file_integrity.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/auditbeat/tests/system/test_file_integrity.py b/auditbeat/tests/system/test_file_integrity.py index 252a372fa14..6af6d3c0fe6 100644 --- a/auditbeat/tests/system/test_file_integrity.py +++ b/auditbeat/tests/system/test_file_integrity.py @@ -32,12 +32,6 @@ def is_platform_supported(): return {'aarch64', 'arm64', 'x86_64', 'amd64'}.intersection(p) -if platform.system() == 'Linux': - fim_backends = ["fsnotify", "kprobes"] -else: - fim_backends = ["fsnotify"] - - # Escapes a path to match what's printed in the logs def escape_path(path): return path.replace('\\', '\\\\') @@ -86,13 +80,13 @@ class Test(BaseTest): def wait_output(self, min_events): self.wait_until(lambda: wrap_except(lambda: len(self.read_output()) >= min_events)) - # wait for the number of lines in the file to stay constant for 10 seconds + # wait for the number of lines in the file to stay constant for a second prev_lines = -1 while True: num_lines = self.output_lines() if prev_lines < num_lines: prev_lines = num_lines - time.sleep(10) + time.sleep(1) else: break From e745e23339cb32fd783ce8f774a0bf22a8aed4bc Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 14 Feb 2024 00:59:13 +0200 Subject: [PATCH 35/39] fix: gofumpt kprobes package --- .../module/file_integrity/kprobes/events_cache.go | 6 ++++-- .../file_integrity/kprobes/events_cache_test.go | 5 +---- .../module/file_integrity/kprobes/events_verifier.go | 12 +++++------- .../file_integrity/kprobes/events_verifier_test.go | 2 -- auditbeat/module/file_integrity/kprobes/executor.go | 1 - .../module/file_integrity/kprobes/executor_test.go | 1 - .../module/file_integrity/kprobes/kallsyms_test.go | 1 - auditbeat/module/file_integrity/kprobes/monitor.go | 1 - .../module/file_integrity/kprobes/monitor_test.go | 1 - auditbeat/module/file_integrity/kprobes/path.go | 3 --- .../file_integrity/kprobes/path_inotify_test.go | 3 --- .../module/file_integrity/kprobes/path_mountpoint.go | 6 ++---- auditbeat/module/file_integrity/kprobes/path_test.go | 1 - .../module/file_integrity/kprobes/probes_fsnotify.go | 1 - .../kprobes/probes_fsnotify_nameremove.go | 1 - .../kprobes/probes_fsnotify_nameremove_test.go | 1 - .../file_integrity/kprobes/probes_fsnotify_parent.go | 1 - .../kprobes/probes_fsnotify_parent_test.go | 1 - .../file_integrity/kprobes/probes_fsnotify_test.go | 1 - .../file_integrity/kprobes/probes_vfs_getattr.go | 1 - .../kprobes/probes_vfs_getattr_test.go | 1 - auditbeat/module/file_integrity/kprobes/verifier.go | 2 -- 22 files changed, 12 insertions(+), 41 deletions(-) diff --git a/auditbeat/module/file_integrity/kprobes/events_cache.go b/auditbeat/module/file_integrity/kprobes/events_cache.go index e53b7ead75e..d8559535ad1 100644 --- a/auditbeat/module/file_integrity/kprobes/events_cache.go +++ b/auditbeat/module/file_integrity/kprobes/events_cache.go @@ -23,8 +23,10 @@ import ( "path/filepath" ) -type dEntriesIndex map[dKey]*dEntry -type dEntriesMoveIndex map[uint64]*dEntry +type ( + dEntriesIndex map[dKey]*dEntry + dEntriesMoveIndex map[uint64]*dEntry +) // dEntryCache is a cache of directory entries (dEntries) that exposes appropriate methods to add, get, remove and // handle move operations. Note that dEntryCache is designed to be utilised by a single goroutine at a time and thus diff --git a/auditbeat/module/file_integrity/kprobes/events_cache_test.go b/auditbeat/module/file_integrity/kprobes/events_cache_test.go index 00f7afb846b..ca1a39d4bcb 100644 --- a/auditbeat/module/file_integrity/kprobes/events_cache_test.go +++ b/auditbeat/module/file_integrity/kprobes/events_cache_test.go @@ -28,7 +28,7 @@ import ( ) func (d *dEntryCache) Dump(path string) error { - fileDump, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) + fileDump, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644) if err != nil { return err } @@ -135,7 +135,6 @@ func Test_DirEntryCache_Add(t *testing.T) { } func Test_DirEntryCache_Get(t *testing.T) { - cases := []struct { name string key dKey @@ -609,7 +608,6 @@ func Test_DirEntryCache_GetChild(t *testing.T) { for _, c := range cases { t.Run(c.name, func(t *testing.T) { - for _, child := range c.children { c.entry.AddChild(child) } @@ -621,7 +619,6 @@ func Test_DirEntryCache_GetChild(t *testing.T) { } else { require.NotNil(t, childEntry) } - }) } } diff --git a/auditbeat/module/file_integrity/kprobes/events_verifier.go b/auditbeat/module/file_integrity/kprobes/events_verifier.go index a1870736c88..2a5f902b363 100644 --- a/auditbeat/module/file_integrity/kprobes/events_verifier.go +++ b/auditbeat/module/file_integrity/kprobes/events_verifier.go @@ -37,7 +37,7 @@ type eventID struct { var eventGenerators = []func(*eventsVerifier, string, string) error{ // create file - generates 1 event func(e *eventsVerifier, targetFilePath string, targetMovedFilePath string) error { - file, err := os.OpenFile(targetFilePath, os.O_RDWR|os.O_CREATE, 0644) + file, err := os.OpenFile(targetFilePath, os.O_RDWR|os.O_CREATE, 0o644) if err != nil { return err } @@ -55,7 +55,7 @@ var eventGenerators = []func(*eventsVerifier, string, string) error{ }, // write to file - generates 1 event func(e *eventsVerifier, targetFilePath string, targetMovedFilePath string) error { - file, err := os.OpenFile(targetFilePath, os.O_WRONLY, 0644) + file, err := os.OpenFile(targetFilePath, os.O_WRONLY, 0o644) if err != nil { return err } @@ -76,7 +76,7 @@ var eventGenerators = []func(*eventsVerifier, string, string) error{ }, // change mode of file - generates 1 event func(e *eventsVerifier, targetFilePath string, targetMovedFilePath string) error { - if err := os.Chmod(targetFilePath, 0700); err != nil { + if err := os.Chmod(targetFilePath, 0o700); err != nil { return err } e.addEventToExpect(targetFilePath, unix.IN_ATTRIB) @@ -127,7 +127,7 @@ var eventGenerators = []func(*eventsVerifier, string, string) error{ }, // create a directory - generates 1 event func(e *eventsVerifier, targetFilePath string, targetMovedFilePath string) error { - if err := os.Mkdir(targetFilePath, 0600); err != nil { + if err := os.Mkdir(targetFilePath, 0o600); err != nil { return err } e.addEventToExpect(targetFilePath, unix.IN_CREATE) @@ -135,7 +135,7 @@ var eventGenerators = []func(*eventsVerifier, string, string) error{ }, // change mode of directory - generates 1 event func(e *eventsVerifier, targetFilePath string, targetMovedFilePath string) error { - if err := os.Chmod(targetFilePath, 0644); err != nil { + if err := os.Chmod(targetFilePath, 0o644); err != nil { return err } e.addEventToExpect(targetFilePath, unix.IN_ATTRIB) @@ -178,7 +178,6 @@ var eventGenerators = []func(*eventsVerifier, string, string) error{ // move directory - generates 2 events func(e *eventsVerifier, targetFilePath string, targetMovedFilePath string) error { if err := os.Rename(targetFilePath, targetMovedFilePath); err != nil { - return err } e.addEventToExpect(targetFilePath, unix.IN_MOVED_FROM) @@ -264,7 +263,6 @@ func (e *eventsVerifier) GenerateEvents() error { // Verified checks that all expected events filled during GenerateEvents() are present without any missing // or duplicated. func (e *eventsVerifier) Verified() error { - if e.eventsToExpectNr == 0 { return ErrVerifyNoEventsToExpect } diff --git a/auditbeat/module/file_integrity/kprobes/events_verifier_test.go b/auditbeat/module/file_integrity/kprobes/events_verifier_test.go index 5bd4c955b54..c630f8a2e69 100644 --- a/auditbeat/module/file_integrity/kprobes/events_verifier_test.go +++ b/auditbeat/module/file_integrity/kprobes/events_verifier_test.go @@ -28,7 +28,6 @@ import ( ) func Test_EventsVerifier(t *testing.T) { - type verifierEvents struct { path string op uint32 @@ -175,7 +174,6 @@ func Test_EventsVerifier(t *testing.T) { } func Test_EventsVerifier_GenerateEvents(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "kprobe_unit_test") require.NoError(t, err) diff --git a/auditbeat/module/file_integrity/kprobes/executor.go b/auditbeat/module/file_integrity/kprobes/executor.go index d13b517aca8..2f0485dde3c 100644 --- a/auditbeat/module/file_integrity/kprobes/executor.go +++ b/auditbeat/module/file_integrity/kprobes/executor.go @@ -74,7 +74,6 @@ func (ex *fixedExecutor) Close() { // newFixedThreadExecutor returns a new fixedExecutor. func newFixedThreadExecutor(ctx context.Context) *fixedExecutor { - mCtx, cancelFn := context.WithCancel(ctx) ex := &fixedExecutor{ diff --git a/auditbeat/module/file_integrity/kprobes/executor_test.go b/auditbeat/module/file_integrity/kprobes/executor_test.go index eb9c0395147..c016bbf9766 100644 --- a/auditbeat/module/file_integrity/kprobes/executor_test.go +++ b/auditbeat/module/file_integrity/kprobes/executor_test.go @@ -31,7 +31,6 @@ import ( ) func Test_executor(t *testing.T) { - // parent context is cancelled at creation ctx, cancel := context.WithCancel(context.Background()) cancel() diff --git a/auditbeat/module/file_integrity/kprobes/kallsyms_test.go b/auditbeat/module/file_integrity/kprobes/kallsyms_test.go index 22dcb59e5f4..beeb4693cf4 100644 --- a/auditbeat/module/file_integrity/kprobes/kallsyms_test.go +++ b/auditbeat/module/file_integrity/kprobes/kallsyms_test.go @@ -27,7 +27,6 @@ import ( ) func Test_getSymbolInfoFromReader(t *testing.T) { - content := `0000000000000000 t fsnotify_move 0000000000000000 T fsnotify 0000000000000000 T fsnotifyy diff --git a/auditbeat/module/file_integrity/kprobes/monitor.go b/auditbeat/module/file_integrity/kprobes/monitor.go index 089e246598c..1b3cef35aed 100644 --- a/auditbeat/module/file_integrity/kprobes/monitor.go +++ b/auditbeat/module/file_integrity/kprobes/monitor.go @@ -93,7 +93,6 @@ func New(isRecursive bool) (*Monitor, error) { } func newMonitor(ctx context.Context, isRecursive bool, pChannel perfChannel, exec executor) (*Monitor, error) { - mCtx, cancelFunc := context.WithCancel(ctx) p, err := newPathMonitor(mCtx, exec, 0, isRecursive) diff --git a/auditbeat/module/file_integrity/kprobes/monitor_test.go b/auditbeat/module/file_integrity/kprobes/monitor_test.go index 0c075238975..da1021d5bbf 100644 --- a/auditbeat/module/file_integrity/kprobes/monitor_test.go +++ b/auditbeat/module/file_integrity/kprobes/monitor_test.go @@ -470,7 +470,6 @@ func (p *monitorTestSuite) TestRunEmitError() { } func (p *monitorTestSuite) TestNew() { - if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" { p.T().Skip("skipping on non-amd64/arm64") return diff --git a/auditbeat/module/file_integrity/kprobes/path.go b/auditbeat/module/file_integrity/kprobes/path.go index 229603570c9..982c44ec240 100644 --- a/auditbeat/module/file_integrity/kprobes/path.go +++ b/auditbeat/module/file_integrity/kprobes/path.go @@ -172,12 +172,10 @@ func (r *pTraverser) WalkAsync(path string, depth uint32, tid uint32) { case r.errC <- walkErr: case <-r.ctx.Done(): } - }() } func (r *pTraverser) walkRecursive(ctx context.Context, path string, mounts mountPoints, depth uint32, isFromMove bool, tid uint32) error { - if ctx.Err() != nil { return ctx.Err() } @@ -285,7 +283,6 @@ func (r *pTraverser) waitForWalk(ctx context.Context) error { } func (r *pTraverser) walk(ctx context.Context, path string, depth uint32, isFromMove bool, tid uint32) error { - // get a snapshot of all mountpoints mounts, err := getAllMountPoints() if err != nil { diff --git a/auditbeat/module/file_integrity/kprobes/path_inotify_test.go b/auditbeat/module/file_integrity/kprobes/path_inotify_test.go index 51608d751ce..2cc0f37e51a 100644 --- a/auditbeat/module/file_integrity/kprobes/path_inotify_test.go +++ b/auditbeat/module/file_integrity/kprobes/path_inotify_test.go @@ -29,7 +29,6 @@ import ( ) func Test_InotifyWatcher(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "kprobe_unit_test") require.NoError(t, err) defer os.RemoveAll(tmpDir) @@ -64,7 +63,6 @@ func Test_InotifyWatcher(t *testing.T) { } func Test_InotifyWatcher_Add_Err(t *testing.T) { - watcher, err := newInotifyWatcher() require.NoError(t, err) @@ -82,7 +80,6 @@ func Test_InotifyWatcher_Add_Err(t *testing.T) { } func Test_InotifyWatcher_Close_Err(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "kprobe_unit_test") require.NoError(t, err) defer os.RemoveAll(tmpDir) diff --git a/auditbeat/module/file_integrity/kprobes/path_mountpoint.go b/auditbeat/module/file_integrity/kprobes/path_mountpoint.go index 6029061c83e..9b4f66309f0 100644 --- a/auditbeat/module/file_integrity/kprobes/path_mountpoint.go +++ b/auditbeat/module/file_integrity/kprobes/path_mountpoint.go @@ -30,10 +30,8 @@ import ( "sync" ) -var ( - // Used to make the mount functions thread safe - mountMutex sync.Mutex -) +// Used to make the mount functions thread safe +var mountMutex sync.Mutex // mount contains information for a specific mounted filesystem. // diff --git a/auditbeat/module/file_integrity/kprobes/path_test.go b/auditbeat/module/file_integrity/kprobes/path_test.go index cd0bc255535..1689bf5a633 100644 --- a/auditbeat/module/file_integrity/kprobes/path_test.go +++ b/auditbeat/module/file_integrity/kprobes/path_test.go @@ -190,7 +190,6 @@ func (p *pathTestSuite) TestRecursiveWalkAsync() { } func (p *pathTestSuite) TestWalkAsyncTimeoutErr() { - tmpDir, err := os.MkdirTemp("", "kprobe_unit_test") p.Require().NoError(err) defer os.RemoveAll(tmpDir) diff --git a/auditbeat/module/file_integrity/kprobes/probes_fsnotify.go b/auditbeat/module/file_integrity/kprobes/probes_fsnotify.go index f840e891a96..39f944377ea 100644 --- a/auditbeat/module/file_integrity/kprobes/probes_fsnotify.go +++ b/auditbeat/module/file_integrity/kprobes/probes_fsnotify.go @@ -36,7 +36,6 @@ type fsNotifySymbol struct { } func loadFsNotifySymbol(s *probeManager) error { - symbolInfo, err := s.getSymbolInfoRuntime("fsnotify") if err != nil { return err diff --git a/auditbeat/module/file_integrity/kprobes/probes_fsnotify_nameremove.go b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_nameremove.go index 3409d855864..ecabb94c7d2 100644 --- a/auditbeat/module/file_integrity/kprobes/probes_fsnotify_nameremove.go +++ b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_nameremove.go @@ -31,7 +31,6 @@ type fsNotifyNameRemoveSymbol struct { } func loadFsNotifyNameRemoveSymbol(s *probeManager) error { - symbolInfo, err := s.getSymbolInfoRuntime("fsnotify_nameremove") if err != nil { if errors.Is(err, ErrSymbolNotFound) { diff --git a/auditbeat/module/file_integrity/kprobes/probes_fsnotify_nameremove_test.go b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_nameremove_test.go index 17e7b3d9f49..90169b697b3 100644 --- a/auditbeat/module/file_integrity/kprobes/probes_fsnotify_nameremove_test.go +++ b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_nameremove_test.go @@ -104,5 +104,4 @@ func Test_fsNotifyNameRemoveSymbol_onErr(t *testing.T) { testErr := fmt.Errorf("test: %w", ErrVerifyOverlappingEvents) repeat := s.onErr(testErr) require.False(t, repeat) - } diff --git a/auditbeat/module/file_integrity/kprobes/probes_fsnotify_parent.go b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_parent.go index 505d40f6b08..5b128273675 100644 --- a/auditbeat/module/file_integrity/kprobes/probes_fsnotify_parent.go +++ b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_parent.go @@ -32,7 +32,6 @@ type fsNotifyParentSymbol struct { } func loadFsNotifyParentSymbol(s *probeManager) error { - symbolInfo, err := s.getSymbolInfoRuntime("__fsnotify_parent") if err != nil { if !errors.Is(err, ErrSymbolNotFound) { diff --git a/auditbeat/module/file_integrity/kprobes/probes_fsnotify_parent_test.go b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_parent_test.go index 5198cb673cd..4d4ea4bb47f 100644 --- a/auditbeat/module/file_integrity/kprobes/probes_fsnotify_parent_test.go +++ b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_parent_test.go @@ -138,5 +138,4 @@ func Test_fsNotifyParentSymbol_onErr(t *testing.T) { testErr := fmt.Errorf("test: %w", ErrVerifyOverlappingEvents) repeat := s.onErr(testErr) require.False(t, repeat) - } diff --git a/auditbeat/module/file_integrity/kprobes/probes_fsnotify_test.go b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_test.go index ba02cbf4638..9392deffe72 100644 --- a/auditbeat/module/file_integrity/kprobes/probes_fsnotify_test.go +++ b/auditbeat/module/file_integrity/kprobes/probes_fsnotify_test.go @@ -98,5 +98,4 @@ func Test_fsNotifySymbol_onErr(t *testing.T) { require.False(t, s.onErr(ErrVerifyMissingEvents)) require.False(t, s.onErr(ErrVerifyUnexpectedEvent)) - } diff --git a/auditbeat/module/file_integrity/kprobes/probes_vfs_getattr.go b/auditbeat/module/file_integrity/kprobes/probes_vfs_getattr.go index e115eba4565..6bc0f4ab01f 100644 --- a/auditbeat/module/file_integrity/kprobes/probes_vfs_getattr.go +++ b/auditbeat/module/file_integrity/kprobes/probes_vfs_getattr.go @@ -32,7 +32,6 @@ type vfsGetAttrSymbol struct { } func loadVFSGetAttrSymbol(s *probeManager, e executor) error { - // get the vfs_getattr_nosec symbol information symbolInfo, err := s.getSymbolInfoRuntime("vfs_getattr_nosec") if err != nil { diff --git a/auditbeat/module/file_integrity/kprobes/probes_vfs_getattr_test.go b/auditbeat/module/file_integrity/kprobes/probes_vfs_getattr_test.go index 3b926599316..114f026e179 100644 --- a/auditbeat/module/file_integrity/kprobes/probes_vfs_getattr_test.go +++ b/auditbeat/module/file_integrity/kprobes/probes_vfs_getattr_test.go @@ -145,5 +145,4 @@ func Test_vfsGetAttr_onErr(t *testing.T) { testErr := fmt.Errorf("test: %w", ErrVerifyOverlappingEvents) repeat := s.onErr(testErr) require.False(t, repeat) - } diff --git a/auditbeat/module/file_integrity/kprobes/verifier.go b/auditbeat/module/file_integrity/kprobes/verifier.go index ccf0797c412..0ea1cf57f1e 100644 --- a/auditbeat/module/file_integrity/kprobes/verifier.go +++ b/auditbeat/module/file_integrity/kprobes/verifier.go @@ -38,7 +38,6 @@ import ( var embedBTFFolder embed.FS func getVerifiedProbes(ctx context.Context, timeout time.Duration) (map[tracing.Probe]tracing.AllocateFn, executor, error) { - fExec := newFixedThreadExecutor(ctx) probeMgr, err := newProbeManager(fExec) @@ -127,7 +126,6 @@ func loadEmbeddedSpecs() ([]*tkbtf.Spec, error) { specs = append(specs, embedSpec) return nil }) - if err != nil { return nil, err } From 4a12aa9f3948587253f3c11637a5f752be9a4836 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 14 Feb 2024 01:02:03 +0200 Subject: [PATCH 36/39] fix: highlight unused context in event processor --- auditbeat/module/file_integrity/kprobes/events_process.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auditbeat/module/file_integrity/kprobes/events_process.go b/auditbeat/module/file_integrity/kprobes/events_process.go index d0ba1619647..23e8a110f58 100644 --- a/auditbeat/module/file_integrity/kprobes/events_process.go +++ b/auditbeat/module/file_integrity/kprobes/events_process.go @@ -47,7 +47,7 @@ func newEventProcessor(p pathTraverser, e Emitter, isRecursive bool) *eProcessor } } -func (e *eProcessor) process(ctx context.Context, pe *ProbeEvent) error { +func (e *eProcessor) process(_ context.Context, pe *ProbeEvent) error { // after processing return the probe event to the pool defer releaseProbeEvent(pe) From d80fbf5acfb02bb6a65582f278f7275147203c97 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 14 Feb 2024 01:08:12 +0200 Subject: [PATCH 37/39] fix: increase interval period of wait_output as kprobes require more time to verify the probes and print output --- auditbeat/tests/system/test_file_integrity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/auditbeat/tests/system/test_file_integrity.py b/auditbeat/tests/system/test_file_integrity.py index 3a379194909..e6b03306c3a 100644 --- a/auditbeat/tests/system/test_file_integrity.py +++ b/auditbeat/tests/system/test_file_integrity.py @@ -78,13 +78,13 @@ def wrap_except(expr): class Test(BaseTest): def wait_output(self, min_events): self.wait_until(lambda: wrap_except(lambda: len(self.read_output()) >= min_events)) - # wait for the number of lines in the file to stay constant for a second + # wait for the number of lines in the file to stay constant for 10 seconds prev_lines = -1 while True: num_lines = self.output_lines() if prev_lines < num_lines: prev_lines = num_lines - time.sleep(1) + time.sleep(10) else: break From bd8d23a82f7c0e982face1e40a1441363309de14 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 14 Feb 2024 01:09:43 +0200 Subject: [PATCH 38/39] fix: proper formatting for auditbeat.reference.yml --- auditbeat/auditbeat.reference.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/auditbeat/auditbeat.reference.yml b/auditbeat/auditbeat.reference.yml index 54c7f2f4b96..e9a23ca6ac0 100644 --- a/auditbeat/auditbeat.reference.yml +++ b/auditbeat/auditbeat.reference.yml @@ -174,7 +174,7 @@ auditbeat.modules: # - file.pe.go_imports_names_entropy # - file.pe.go_imports_names_var_entropy # - file.pe.go_stripped - + # ================================== General =================================== @@ -1189,7 +1189,7 @@ output.elasticsearch: # Permissions to use for file creation. The default is 0600. #permissions: 0600 - + # Configure automatic file rotation on every startup. The default is true. #rotate_on_startup: true @@ -1366,7 +1366,7 @@ setup.template.settings: # ======================== Data Stream Lifecycle (DSL) ========================= -# Configure Data Stream Lifecycle to manage data streams while connected to Serverless elasticsearch. +# Configure Data Stream Lifecycle to manage data streams while connected to Serverless elasticsearch. # These settings are mutually exclusive with ILM settings which are not supported in Serverless projects. # Enable DSL support. Valid values are true, or false. @@ -1374,7 +1374,7 @@ setup.template.settings: # Set the lifecycle policy name or pattern. For DSL, this name must match the data stream that the lifecycle is for. # The default data stream pattern is auditbeat-%{[agent.version]}" -# The template string `%{[agent.version]}` will resolve to the current stack version. +# The template string `%{[agent.version]}` will resolve to the current stack version. # The other possible template value is `%{[beat.name]}`. #setup.dsl.data_stream_pattern: "auditbeat-%{[agent.version]}" From f1e51f407c60ae398b37665fcf5534e20d5a1a94 Mon Sep 17 00:00:00 2001 From: Panos Koutsovasilis Date: Wed, 14 Feb 2024 01:17:28 +0200 Subject: [PATCH 39/39] fix: proper formatting for x-pack/auditbeat/auditbeat.reference.yml --- x-pack/auditbeat/auditbeat.reference.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/auditbeat/auditbeat.reference.yml b/x-pack/auditbeat/auditbeat.reference.yml index 8979b18e5d9..a0352454e09 100644 --- a/x-pack/auditbeat/auditbeat.reference.yml +++ b/x-pack/auditbeat/auditbeat.reference.yml @@ -174,7 +174,7 @@ auditbeat.modules: # - file.pe.go_imports_names_entropy # - file.pe.go_imports_names_var_entropy # - file.pe.go_stripped - + # The system module collects security related information about a host. # All datasets send both periodic state information (e.g. all currently # running processes) and real-time changes (e.g. when a new process starts @@ -1245,7 +1245,7 @@ output.elasticsearch: # Permissions to use for file creation. The default is 0600. #permissions: 0600 - + # Configure automatic file rotation on every startup. The default is true. #rotate_on_startup: true @@ -1422,7 +1422,7 @@ setup.template.settings: # ======================== Data Stream Lifecycle (DSL) ========================= -# Configure Data Stream Lifecycle to manage data streams while connected to Serverless elasticsearch. +# Configure Data Stream Lifecycle to manage data streams while connected to Serverless elasticsearch. # These settings are mutually exclusive with ILM settings which are not supported in Serverless projects. # Enable DSL support. Valid values are true, or false. @@ -1430,7 +1430,7 @@ setup.template.settings: # Set the lifecycle policy name or pattern. For DSL, this name must match the data stream that the lifecycle is for. # The default data stream pattern is auditbeat-%{[agent.version]}" -# The template string `%{[agent.version]}` will resolve to the current stack version. +# The template string `%{[agent.version]}` will resolve to the current stack version. # The other possible template value is `%{[beat.name]}`. #setup.dsl.data_stream_pattern: "auditbeat-%{[agent.version]}"