From 243ded28c1ead5ec1324069d80bf77f57445c228 Mon Sep 17 00:00:00 2001 From: Mattia Meleleo Date: Thu, 16 Nov 2023 01:33:25 +0100 Subject: [PATCH] fim: implement ebpf backend --- ...heck-audtibeat.yml => check-auditbeat.yml} | 0 CHANGELOG.next.asciidoc | 43 +--- NOTICE.txt | 98 ++++++++- auditbeat/.gitignore | 1 - auditbeat/auditbeat.reference.yml | 4 + auditbeat/docker-compose.yml | 1 + .../docs/modules/file_integrity.asciidoc | 8 +- auditbeat/internal/ebpf/seccomp_linux.go | 40 ++++ auditbeat/internal/ebpf/types.go | 27 +++ auditbeat/internal/ebpf/watcher_linux.go | 167 +++++++++++++++ auditbeat/internal/ebpf/watcher_other.go | 28 +++ auditbeat/internal/ebpf/watcher_test.go | 61 ++++++ .../file_integrity/_meta/config.yml.tmpl | 7 + .../module/file_integrity/_meta/docs.asciidoc | 8 +- auditbeat/module/file_integrity/config.go | 20 ++ auditbeat/module/file_integrity/event.go | 71 ++++--- .../module/file_integrity/event_linux.go | 199 ++++++++++++++++++ .../module/file_integrity/event_linux_test.go | 74 +++++++ .../module/file_integrity/eventreader_ebpf.go | 128 +++++++++++ .../file_integrity/eventreader_fsevents.go | 8 +- .../file_integrity/eventreader_fsnotify.go | 22 +- .../file_integrity/eventreader_linux.go | 55 +++++ .../file_integrity/eventreader_other.go | 34 +++ .../module/file_integrity/fileinfo_posix.go | 29 +-- .../module/file_integrity/flatbuffers.go | 24 ++- auditbeat/module/file_integrity/metricset.go | 18 +- .../module/file_integrity/monitor/monitor.go | 4 +- .../file_integrity/monitor/recursive.go | 4 +- auditbeat/module/file_integrity/schema.fbs | 5 + .../module/file_integrity/schema/Source.go | 3 + .../module/file_integrity/schema/Type.go | 36 ++-- auditbeat/tests/system/test_file_integrity.py | 182 ++++++++++++---- dev-tools/notice/overrides.json | 1 + go.mod | 2 + go.sum | 13 +- x-pack/auditbeat/auditbeat.reference.yml | 4 + 36 files changed, 1253 insertions(+), 176 deletions(-) rename .github/workflows/{check-audtibeat.yml => check-auditbeat.yml} (100%) create mode 100644 auditbeat/internal/ebpf/seccomp_linux.go create mode 100644 auditbeat/internal/ebpf/types.go create mode 100644 auditbeat/internal/ebpf/watcher_linux.go create mode 100644 auditbeat/internal/ebpf/watcher_other.go create mode 100644 auditbeat/internal/ebpf/watcher_test.go create mode 100644 auditbeat/module/file_integrity/event_linux.go create mode 100644 auditbeat/module/file_integrity/event_linux_test.go create mode 100644 auditbeat/module/file_integrity/eventreader_ebpf.go create mode 100644 auditbeat/module/file_integrity/eventreader_linux.go create mode 100644 auditbeat/module/file_integrity/eventreader_other.go diff --git a/.github/workflows/check-audtibeat.yml b/.github/workflows/check-auditbeat.yml similarity index 100% rename from .github/workflows/check-audtibeat.yml rename to .github/workflows/check-auditbeat.yml diff --git a/CHANGELOG.next.asciidoc b/CHANGELOG.next.asciidoc index 48d87b1dd86f..da8d65e5afe1 100644 --- a/CHANGELOG.next.asciidoc +++ b/CHANGELOG.next.asciidoc @@ -136,6 +136,7 @@ Setting environmental variable ELASTIC_NETINFO:false in Elastic Agent pod will d *Auditbeat* +- Add opt-in eBPF backend for file_integrity module. {pull}37223[37223] *Filebeat* @@ -261,45 +262,3 @@ Setting environmental variable ELASTIC_NETINFO:false in Elastic Agent pod will d ==== Known Issues - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/NOTICE.txt b/NOTICE.txt index 7e0e27d091c7..23bc17732aa5 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -12255,6 +12255,32 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +-------------------------------------------------------------------------------- +Dependency : github.com/elastic/ebpfevents +Version: v0.3.3-0.20240131115527-3312e650a303 +Licence type (autodetected): Apache-2.0 +-------------------------------------------------------------------------------- + +Contents of probable licence file $GOMODCACHE/github.com/elastic/ebpfevents@v0.3.3-0.20240131115527-3312e650a303/LICENSE.txt: + +The https://github.com/elastic/ebpfevents repository contains source code under +various licenses: + +- Source code in the 'headers/bpf' directory, is dual-licensed under the GNU Lesser General + Public License version 2.1 (LICENSES/LGPL-2.1-only.txt) OR BSD-2-Clause license + (LICENSES/BSD-2-Clause.txt) + +- Source code in the 'ebpf' submodule is licensed with multiple licenses. Read more at + https://github.com/elastic/ebpf/blob/main/LICENSE.txt. + +- The binary files 'bpf_bpfel_x86.o' and 'bpf_bpfel_amd64.o' are compiled + from dual-licensed GPL-2.0-only OR BSD-2-Clause licensed code, and are distributed with + the GPL-2.0-only License (LICENSES/GPL-2.0-only.txt). + +- Source code not listed in the previous points is licensed under the Apache License, + version 2 (LICENSES/Apache-2.0.txt). + + -------------------------------------------------------------------------------- Dependency : github.com/elastic/elastic-agent-autodiscover Version: v0.6.7 @@ -36167,6 +36193,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 +38231,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 @@ -38201,6 +38260,37 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +-------------------------------------------------------------------------------- +Dependency : github.com/go-faker/faker/v4 +Version: v4.2.0 +Licence type (autodetected): MIT +-------------------------------------------------------------------------------- + +Contents of probable licence file $GOMODCACHE/github.com/go-faker/faker/v4@v4.2.0/LICENSE: + +MIT License + +Copyright (c) 2017 Iman Tumorang + +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/go-logfmt/logfmt Version: v0.5.1 @@ -45643,11 +45733,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 diff --git a/auditbeat/.gitignore b/auditbeat/.gitignore index 3cd551fd5066..7c8dbc055013 100644 --- a/auditbeat/.gitignore +++ b/auditbeat/.gitignore @@ -6,4 +6,3 @@ module/*/_meta/config.yml /auditbeat /auditbeat.test /docs/html_docs - diff --git a/auditbeat/auditbeat.reference.yml b/auditbeat/auditbeat.reference.yml index 883760ab410b..381c36b51577 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, 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/docker-compose.yml b/auditbeat/docker-compose.yml index adf338889883..2d7dd8570e08 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/kernel/tracing/:/sys/kernel/tracing/ command: make privileged: true pid: host diff --git a/auditbeat/docs/modules/file_integrity.asciidoc b/auditbeat/docs/modules/file_integrity.asciidoc index a12c4df47ca0..99c4a05429e7 100644 --- a/auditbeat/docs/modules/file_integrity.asciidoc +++ b/auditbeat/docs/modules/file_integrity.asciidoc @@ -28,8 +28,11 @@ 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. +* Linux - As of now, two kernel backends are supported: `ebpf` 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 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, @@ -144,6 +147,9 @@ of this directories are watched. If `recursive` is set to `true`, the `file_integrity` module will watch for changes on this directories and all their subdirectories. +*`force_backend`*:: (*Linux only*) Select the backend which will be used to +source events. Valid values: `ebpf`, `fsnotify`. Default: `fsnotify`. + include::{docdir}/auditbeat-options.asciidoc[] diff --git a/auditbeat/internal/ebpf/seccomp_linux.go b/auditbeat/internal/ebpf/seccomp_linux.go new file mode 100644 index 000000000000..9059eb0f6433 --- /dev/null +++ b/auditbeat/internal/ebpf/seccomp_linux.go @@ -0,0 +1,40 @@ +// 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 ebpf + +import ( + "runtime" + + "github.com/elastic/beats/v7/libbeat/common/seccomp" +) + +func init() { + switch runtime.GOARCH { + case "amd64": + syscalls := []string{ + "bpf", + "eventfd2", // needed by ringbuf + "perf_event_open", // needed by tracepoints + } + if err := seccomp.ModifyDefaultPolicy(seccomp.AddSyscall, syscalls...); err != nil { + panic(err) + } + } +} diff --git a/auditbeat/internal/ebpf/types.go b/auditbeat/internal/ebpf/types.go new file mode 100644 index 000000000000..e5e72573e201 --- /dev/null +++ b/auditbeat/internal/ebpf/types.go @@ -0,0 +1,27 @@ +// 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 ebpf + +import "github.com/elastic/ebpfevents" + +type EventMask uint64 + +type Watcher interface { + Subscribe(string, EventMask) <-chan ebpfevents.Record + Unsubscribe(string) +} diff --git a/auditbeat/internal/ebpf/watcher_linux.go b/auditbeat/internal/ebpf/watcher_linux.go new file mode 100644 index 000000000000..437f751d1bf0 --- /dev/null +++ b/auditbeat/internal/ebpf/watcher_linux.go @@ -0,0 +1,167 @@ +// 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 ebpf + +import ( + "context" + "fmt" + "sync" + + "github.com/elastic/ebpfevents" +) + +var ( + gWatcherOnce sync.Once + gWatcherErr error + gWatcher watcher +) + +type client struct { + name string + mask EventMask + records chan ebpfevents.Record +} + +type watcher struct { + sync.Mutex + ctx context.Context + cancel context.CancelFunc + loader *ebpfevents.Loader + clients map[string]client + status status +} + +type status int + +const ( + stopped status = iota + started +) + +func GetWatcher() (Watcher, error) { + gWatcher.Lock() + defer gWatcher.Unlock() + + // Try to load the probe once on startup so consumers can error out. + gWatcherOnce.Do(func() { + if gWatcher.status == stopped { + l, err := ebpfevents.NewLoader() + if err != nil { + gWatcherErr = fmt.Errorf("init ebpf loader: %w", err) + return + } + _ = l.Close() + } + }) + + return &gWatcher, gWatcherErr +} + +func (w *watcher) Subscribe(name string, events EventMask) <-chan ebpfevents.Record { + w.Lock() + defer w.Unlock() + + if w.status == stopped { + startLocked() + } + + w.clients[name] = client{ + name: name, + mask: events, + records: make(chan ebpfevents.Record, w.loader.BufferLen()), + } + + return w.clients[name].records +} + +func (w *watcher) Unsubscribe(name string) { + w.Lock() + defer w.Unlock() + + delete(w.clients, name) + + if w.nclients() == 0 { + stopLocked() + } +} + +func startLocked() { + loader, err := ebpfevents.NewLoader() + if err != nil { + gWatcherErr = fmt.Errorf("start ebpf loader: %w", err) + return + } + + gWatcher.loader = loader + gWatcher.clients = make(map[string]client) + + records := make(chan ebpfevents.Record, loader.BufferLen()) + gWatcher.ctx, gWatcher.cancel = context.WithCancel(context.Background()) + + go gWatcher.loader.EventLoop(gWatcher.ctx, records) + go func() { + for { + select { + case r := <-records: + if r.Error != nil { + for _, client := range gWatcher.clients { + client.records <- r + } + continue + } + for _, client := range gWatcher.clients { + if client.mask&EventMask(r.Event.Type) != 0 { + client.records <- r + } + } + continue + case <-gWatcher.ctx.Done(): + return + } + } + }() + + gWatcher.status = started +} + +func stopLocked() { + _ = gWatcher.close() + gWatcher.status = stopped +} + +func (w *watcher) nclients() int { + return len(w.clients) +} + +func (w *watcher) close() error { + if w.cancel != nil { + w.cancel() + } + + if w.loader != nil { + _ = w.loader.Close() + } + + for _, cl := range w.clients { + close(cl.records) + } + + return nil +} diff --git a/auditbeat/internal/ebpf/watcher_other.go b/auditbeat/internal/ebpf/watcher_other.go new file mode 100644 index 000000000000..fc9da1b4cb85 --- /dev/null +++ b/auditbeat/internal/ebpf/watcher_other.go @@ -0,0 +1,28 @@ +// 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 ebpf + +import "errors" + +var ErrNotSupported = errors.New("not supported") + +func NewWatcher() (Watcher, error) { + return nil, ErrNotSupported +} diff --git a/auditbeat/internal/ebpf/watcher_test.go b/auditbeat/internal/ebpf/watcher_test.go new file mode 100644 index 000000000000..13d27ffd52c0 --- /dev/null +++ b/auditbeat/internal/ebpf/watcher_test.go @@ -0,0 +1,61 @@ +// 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 ebpf + +import ( + "math" + "testing" + + "github.com/stretchr/testify/assert" +) + +const allEvents = EventMask(math.MaxUint64) + +func TestWatcherStartStop(t *testing.T) { + w, err := GetWatcher() + if err != nil { + t.Skipf("skipping ebpf watcher test: %v", err) + } + assert.Equal(t, gWatcher.status, stopped) + assert.Equal(t, 0, gWatcher.nclients()) + + _ = w.Subscribe("test-1", allEvents) + assert.Equal(t, gWatcher.status, started) + assert.Equal(t, 1, gWatcher.nclients()) + + _ = w.Subscribe("test-2", allEvents) + assert.Equal(t, 2, gWatcher.nclients()) + + w.Unsubscribe("test-2") + assert.Equal(t, 1, gWatcher.nclients()) + + w.Unsubscribe("dummy") + assert.Equal(t, 1, gWatcher.nclients()) + + assert.Equal(t, gWatcher.status, started) + w.Unsubscribe("test-1") + assert.Equal(t, 0, gWatcher.nclients()) + assert.Equal(t, gWatcher.status, stopped) + + _ = w.Subscribe("new", allEvents) + assert.Equal(t, 1, gWatcher.nclients()) + assert.Equal(t, gWatcher.status, started) + w.Unsubscribe("new") +} diff --git a/auditbeat/module/file_integrity/_meta/config.yml.tmpl b/auditbeat/module/file_integrity/_meta/config.yml.tmpl index 588a6279eee2..1dd9d7f25ef5 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, 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 0f32ef64f930..80d1fc2a7305 100644 --- a/auditbeat/module/file_integrity/_meta/docs.asciidoc +++ b/auditbeat/module/file_integrity/_meta/docs.asciidoc @@ -21,8 +21,11 @@ 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. +* Linux - As of now, two kernel backends are supported: `ebpf` 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 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, @@ -137,4 +140,7 @@ of this directories are watched. If `recursive` is set to `true`, the `file_integrity` module will watch for changes on this directories and all their subdirectories. +*`force_backend`*:: (*Linux only*) Select the backend which will be used to +source events. Valid values: `ebpf`, `fsnotify`. Default: `fsnotify`. + include::{docdir}/auditbeat-options.asciidoc[] diff --git a/auditbeat/module/file_integrity/config.go b/auditbeat/module/file_integrity/config.go index e431e6407667..c86cd1ad171f 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,18 @@ const ( XXH64 HashType = "xxh64" ) +type Backend string + +const ( + BackendFSNotify Backend = "fsnotify" + BackendEBPF Backend = "ebpf" +) + +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 +100,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 +175,11 @@ nextHash: if err != nil { errs = append(errs, fmt.Errorf("invalid scan_rate_per_sec value: %w", err)) } + + if c.ForceBackend == BackendEBPF && runtime.GOOS != "linux" { + errs = append(errs, errors.New("force_backend can only be specified on linux")) + } + return errs.Err() } diff --git a/auditbeat/module/file_integrity/event.go b/auditbeat/module/file_integrity/event.go index fd4d68828a44..c7dfb7032e80 100644 --- a/auditbeat/module/file_integrity/event.go +++ b/auditbeat/module/file_integrity/event.go @@ -65,11 +65,14 @@ const ( // SourceFSNotify identifies events triggered by a notification from the // file system. SourceFSNotify + // SourceEBPF identifies events triggered by an eBPF program. + SourceEBPF ) var sourceNames = map[Source]string{ SourceScan: "scan", SourceFSNotify: "fsnotify", + SourceEBPF: "ebpf", } // Type identifies the file type (e.g. dir, file, symlink). @@ -91,12 +94,20 @@ const ( FileType DirType SymlinkType + CharDeviceType + BlockDeviceType + FIFOType + SocketType ) var typeNames = map[Type]string{ - FileType: "file", - DirType: "dir", - SymlinkType: "symlink", + FileType: "file", + DirType: "dir", + SymlinkType: "symlink", + CharDeviceType: "char_device", + BlockDeviceType: "block_device", + FIFOType: "fifo", + SocketType: "socket", } // Digest is an output of a hash function. @@ -189,36 +200,42 @@ func NewEventFromFileInfo( switch event.Info.Type { case FileType: - if event.Info.Size <= maxFileSize { - hashes, nbytes, err := hashFile(event.Path, maxFileSize, hashTypes...) - if err != nil { - event.errors = append(event.errors, err) - event.hashFailed = true - } else if hashes != nil { - // hashFile returns nil hashes and no error when: - // - There's no hashes configured. - // - File size at the time of hashing is larger than configured limit. - event.Hashes = hashes - event.Info.Size = nbytes - } - - if len(fileParsers) != 0 && event.ParserResults == nil { - event.ParserResults = make(mapstr.M) - } - for _, p := range fileParsers { - err = p.Parse(event.ParserResults, path) - if err != nil { - event.errors = append(event.errors, err) - } - } - } + fillHashes(&event, path, maxFileSize, hashTypes, fileParsers) case SymlinkType: - event.TargetPath, _ = filepath.EvalSymlinks(event.Path) + event.TargetPath, err = filepath.EvalSymlinks(event.Path) + if err != nil { + event.errors = append(event.errors, err) + } } return event } +func fillHashes(event *Event, path string, maxFileSize uint64, hashTypes []HashType, fileParsers []FileParser) { + if event.Info.Size <= maxFileSize { + hashes, nbytes, err := hashFile(event.Path, maxFileSize, hashTypes...) + if err != nil { + event.errors = append(event.errors, err) + event.hashFailed = true + } else if hashes != nil { + // hashFile returns nil hashes and no error when: + // - There's no hashes configured. + // - File size at the time of hashing is larger than configured limit. + event.Hashes = hashes + event.Info.Size = nbytes + } + + if len(fileParsers) != 0 && event.ParserResults == nil { + event.ParserResults = make(mapstr.M) + } + for _, p := range fileParsers { + if err = p.Parse(event.ParserResults, path); err != nil { + event.errors = append(event.errors, err) + } + } + } +} + // NewEvent creates a new Event. Any errors that occur are included in the // returned Event. func NewEvent( diff --git a/auditbeat/module/file_integrity/event_linux.go b/auditbeat/module/file_integrity/event_linux.go new file mode 100644 index 000000000000..7643d03a6b42 --- /dev/null +++ b/auditbeat/module/file_integrity/event_linux.go @@ -0,0 +1,199 @@ +// 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 ( + "os" + "os/user" + "path/filepath" + "strconv" + "time" + + "github.com/elastic/ebpfevents" +) + +// NewEventFromEbpfEvent creates a new Event from an ebpfevents.Event. +func NewEventFromEbpfEvent( + ee ebpfevents.Event, + maxFileSize uint64, + hashTypes []HashType, + fileParsers []FileParser, + isExcludedPath func(string) bool, +) (Event, bool) { + var ( + path, target string + action Action + metadata Metadata + err error + ) + switch ee.Type { + case ebpfevents.EventTypeFileCreate: + action = Created + + fileCreateEvent := ee.Body.(*ebpfevents.FileCreate) + path = fileCreateEvent.Path + if isExcludedPath(path) { + event := Event{Path: path} + return event, false + } + target = fileCreateEvent.SymlinkTargetPath + metadata, err = metadataFromFileCreate(fileCreateEvent) + case ebpfevents.EventTypeFileRename: + action = Moved + + fileRenameEvent := ee.Body.(*ebpfevents.FileRename) + path = fileRenameEvent.NewPath + if isExcludedPath(path) { + event := Event{Path: path} + return event, false + } + target = fileRenameEvent.SymlinkTargetPath + metadata, err = metadataFromFileRename(fileRenameEvent) + case ebpfevents.EventTypeFileDelete: + action = Deleted + + fileDeleteEvent := ee.Body.(*ebpfevents.FileDelete) + path = fileDeleteEvent.Path + if isExcludedPath(path) { + event := Event{Path: path} + return event, false + } + target = fileDeleteEvent.SymlinkTargetPath + case ebpfevents.EventTypeFileModify: + fileModifyEvent := ee.Body.(*ebpfevents.FileModify) + + switch fileModifyEvent.ChangeType { + case ebpfevents.FileChangeTypeContent: + action = Updated + case ebpfevents.FileChangeTypePermissions, ebpfevents.FileChangeTypeOwner, ebpfevents.FileChangeTypeXattrs: + action = AttributesModified + } + + path = fileModifyEvent.Path + if isExcludedPath(path) { + event := Event{Path: path} + return event, false + } + target = fileModifyEvent.SymlinkTargetPath + metadata, err = metadataFromFileModify(fileModifyEvent) + } + + event := Event{ + Timestamp: time.Now().UTC(), + Path: path, + TargetPath: target, + Info: &metadata, + Source: SourceEBPF, + Action: action, + errors: make([]error, 0), + } + if err != nil { + event.errors = append(event.errors, err) + } + + if event.Action == Deleted { + event.Info = nil + } else { + switch event.Info.Type { + case FileType: + fillHashes(&event, path, maxFileSize, hashTypes, fileParsers) + case SymlinkType: + var err error + event.TargetPath, err = filepath.EvalSymlinks(event.Path) + if err != nil { + event.errors = append(event.errors, err) + } + } + } + + return event, true +} + +func metadataFromFileCreate(evt *ebpfevents.FileCreate) (Metadata, error) { + var md Metadata + fillExtendedAttributes(&md, evt.Path) + err := fillFileInfo(&md, evt.Finfo) + return md, err +} + +func metadataFromFileRename(evt *ebpfevents.FileRename) (Metadata, error) { + var md Metadata + fillExtendedAttributes(&md, evt.NewPath) + err := fillFileInfo(&md, evt.Finfo) + return md, err +} + +func metadataFromFileModify(evt *ebpfevents.FileModify) (Metadata, error) { + var md Metadata + fillExtendedAttributes(&md, evt.Path) + err := fillFileInfo(&md, evt.Finfo) + return md, err +} + +func fillFileInfo(md *Metadata, finfo ebpfevents.FileInfo) error { + md.Inode = finfo.Inode + md.UID = finfo.Uid + md.GID = finfo.Gid + md.Size = finfo.Size + md.MTime = finfo.Mtime + md.CTime = finfo.Ctime + md.Type = typeFromEbpfType(finfo.Type) + md.Mode = finfo.Mode + md.SetUID = finfo.Mode&os.ModeSetuid != 0 + md.SetGID = finfo.Mode&os.ModeSetgid != 0 + + u, err := user.LookupId(strconv.FormatUint(uint64(finfo.Uid), 10)) + if err != nil { + md.Owner = "n/a" + md.Group = "n/a" + return err + } + md.Owner = u.Username + + g, err := user.LookupGroupId(strconv.FormatUint(uint64(finfo.Gid), 10)) + if err != nil { + md.Group = "n/a" + return err + } + md.Group = g.Name + + return nil +} + +func typeFromEbpfType(typ ebpfevents.FileType) Type { + switch typ { + case ebpfevents.FileTypeFile: + return FileType + case ebpfevents.FileTypeDir: + return DirType + case ebpfevents.FileTypeSymlink: + return SymlinkType + case ebpfevents.FileTypeCharDevice: + return CharDeviceType + case ebpfevents.FileTypeBlockDevice: + return BlockDeviceType + case ebpfevents.FileTypeNamedPipe: + return FIFOType + case ebpfevents.FileTypeSocket: + return SocketType + default: + return UnknownType + } +} diff --git a/auditbeat/module/file_integrity/event_linux_test.go b/auditbeat/module/file_integrity/event_linux_test.go new file mode 100644 index 000000000000..1a440afb8f17 --- /dev/null +++ b/auditbeat/module/file_integrity/event_linux_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. + +//go:build linux + +package file_integrity + +import ( + "os" + "os/user" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/elastic/ebpfevents" +) + +func TestNewEventFromEbpfEvent(t *testing.T) { + ebpfEvent := ebpfevents.Event{ + Header: ebpfevents.Header{ + Type: ebpfevents.EventTypeFileCreate, + }, + Body: &ebpfevents.FileCreate{ + Finfo: ebpfevents.FileInfo{ + Type: ebpfevents.FileTypeFile, + Inode: 1234, + Mode: os.FileMode(0o644), + Size: 2345, + Uid: 3456, + Gid: 4567, + }, + Path: "/foo", + SymlinkTargetPath: "/bar", + }, + } + expectedEvent := Event{ + Action: Created, + Path: "/foo", + TargetPath: "/bar", + Info: &Metadata{ + Type: FileType, + Inode: 1234, + UID: 3456, + GID: 4567, + Size: 2345, + Owner: "n/a", + Group: "n/a", + Mode: os.FileMode(0o644), + }, + Source: SourceEBPF, + errors: []error{user.UnknownUserIdError(3456)}, + } + + event, ok := NewEventFromEbpfEvent( + ebpfEvent, 0, []HashType{}, []FileParser{}, func(path string) bool { return false }) + assert.True(t, ok) + event.Timestamp = expectedEvent.Timestamp + + assert.Equal(t, expectedEvent, event) +} diff --git a/auditbeat/module/file_integrity/eventreader_ebpf.go b/auditbeat/module/file_integrity/eventreader_ebpf.go new file mode 100644 index 000000000000..8bae48ed3c6b --- /dev/null +++ b/auditbeat/module/file_integrity/eventreader_ebpf.go @@ -0,0 +1,128 @@ +// 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 ( + "path/filepath" + "strings" + "time" + + "github.com/elastic/beats/v7/auditbeat/internal/ebpf" + "github.com/elastic/ebpfevents" + "github.com/elastic/elastic-agent-libs/logp" +) + +const clientName = "fim" + +type ebpfReader struct { + watcher ebpf.Watcher + done <-chan struct{} + config Config + log *logp.Logger + eventC chan Event + parsers []FileParser + paths map[string]struct{} + + _records <-chan ebpfevents.Record +} + +func (r *ebpfReader) Start(done <-chan struct{}) (<-chan Event, error) { + watcher, err := ebpf.GetWatcher() + if err != nil { + return nil, err + } + r.watcher = watcher + r.done = done + + mask := ebpf.EventMask(ebpfevents.EventTypeFileCreate | ebpfevents.EventTypeFileRename | ebpfevents.EventTypeFileDelete | ebpfevents.EventTypeFileModify) + r._records = r.watcher.Subscribe(clientName, mask) + + go r.consumeEvents() + + r.log.Infow("started ebpf watcher", "file_path", r.config.Paths, "recursive", r.config.Recursive) + return r.eventC, nil +} + +func (r *ebpfReader) consumeEvents() { + defer close(r.eventC) + defer r.watcher.Unsubscribe(clientName) + + for { + select { + case rec := <-r._records: + if rec.Error != nil { + r.log.Errorf("ebpf watcher error: %v", rec.Error) + continue + } + + switch rec.Event.Type { + case ebpfevents.EventTypeFileCreate, ebpfevents.EventTypeFileRename, ebpfevents.EventTypeFileDelete, ebpfevents.EventTypeFileModify: + default: + r.log.Warnf("received unwanted ebpf event: %s", rec.Event.Type.String()) + continue + } + + start := time.Now() + e, ok := NewEventFromEbpfEvent( + *rec.Event, + r.config.MaxFileSizeBytes, + r.config.HashTypes, + r.parsers, + r.excludedPath, + ) + if !ok { + continue + } + e.rtt = time.Since(start) + + r.log.Debugw("received ebpf event", "file_path", e.Path) + r.eventC <- e + case <-r.done: + r.log.Debug("ebpf watcher terminated") + return + } + } +} + +func (r *ebpfReader) excludedPath(path string) bool { + dir, err := filepath.Abs(filepath.Dir(path)) + if err != nil { + r.log.Errorf("ebpf watcher error: resolve abs path %q: %v", path, err) + return true + } + + if r.config.IsExcludedPath(dir) { + return true + } + + if !r.config.Recursive { + if _, ok := r.paths[dir]; ok { + return false + } + } else { + for p := range r.paths { + if strings.HasPrefix(dir, p) { + return false + } + } + } + + return true +} diff --git a/auditbeat/module/file_integrity/eventreader_fsevents.go b/auditbeat/module/file_integrity/eventreader_fsevents.go index 8a5844b3eea1..e6482aea5715 100644 --- a/auditbeat/module/file_integrity/eventreader_fsevents.go +++ b/auditbeat/module/file_integrity/eventreader_fsevents.go @@ -31,7 +31,7 @@ import ( "github.com/elastic/elastic-agent-libs/logp" ) -type fsreader struct { +type fsEventsReader struct { stream *fsevents.EventStream config Config eventC chan Event @@ -129,7 +129,7 @@ func NewEventReader(c Config) (EventProducer, error) { }, nil } -func (r *fsreader) Start(done <-chan struct{}) (<-chan Event, error) { +func (r *fsEventsReader) Start(done <-chan struct{}) (<-chan Event, error) { r.stream.Start() go r.consumeEvents(done) r.log.Infow("Started FSEvents watcher", @@ -138,7 +138,7 @@ func (r *fsreader) Start(done <-chan struct{}) (<-chan Event, error) { return r.eventC, nil } -func (r *fsreader) consumeEvents(done <-chan struct{}) { +func (r *fsEventsReader) consumeEvents(done <-chan struct{}) { defer close(r.eventC) defer r.stream.Stop() @@ -209,7 +209,7 @@ func getFileInfo(path string) (os.FileInfo, error) { return info, fmt.Errorf("failed to stat: %w", err) } -func (r *fsreader) isWatched(path string) bool { +func (r *fsEventsReader) isWatched(path string) bool { if r.config.Recursive { return true } diff --git a/auditbeat/module/file_integrity/eventreader_fsnotify.go b/auditbeat/module/file_integrity/eventreader_fsnotify.go index b49bb7b7905e..0420d0f8f814 100644 --- a/auditbeat/module/file_integrity/eventreader_fsnotify.go +++ b/auditbeat/module/file_integrity/eventreader_fsnotify.go @@ -32,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 @@ -41,16 +41,7 @@ type reader struct { parsers []FileParser } -// NewEventReader creates a new EventProducer backed by fsnotify. -func NewEventReader(c Config) (EventProducer, error) { - 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 @@ -105,17 +96,18 @@ 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{}) []*Event { + events := make([]*Event, 0) for { ev := r.nextEvent(done) if ev == nil { - return + return events } events = append(events, ev) } } -func (r *reader) consumeEvents(done <-chan struct{}) { +func (r *fsNotifyReader) consumeEvents(done <-chan struct{}) { defer close(r.eventC) defer r.watcher.Close() @@ -129,7 +121,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 000000000000..de818de330ad --- /dev/null +++ b/auditbeat/module/file_integrity/eventreader_linux.go @@ -0,0 +1,55 @@ +// 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 ( + "fmt" + + "github.com/elastic/elastic-agent-libs/logp" +) + +func NewEventReader(c Config) (EventProducer, error) { + if c.ForceBackend == BackendEBPF { + l := logp.NewLogger(fmt.Sprintf("%s__ebpf", moduleName)) + l.Info("selected backend: ebpf") + + paths := make(map[string]struct{}) + for _, p := range c.Paths { + paths[p] = struct{}{} + } + + return &ebpfReader{ + config: c, + log: l, + parsers: FileParsers(c), + paths: paths, + eventC: make(chan Event), + }, nil + } + + l := logp.NewLogger(fmt.Sprintf("%s__fsnotify", moduleName)) + l.Info("selected backend: fsnotify") + + return &fsNotifyReader{ + config: c, + log: l, + 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 000000000000..b85892e298f9 --- /dev/null +++ b/auditbeat/module/file_integrity/eventreader_other.go @@ -0,0 +1,34 @@ +// 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 ( + "fmt" + + "github.com/elastic/elastic-agent-libs/logp" +) + +func NewEventReader(c Config) (EventProducer, error) { + return &fsNotifyReader{ + config: c, + log: logp.NewLogger(fmt.Sprintf("%s__fsnotify", moduleName)), + parsers: FileParsers(c), + }, nil +} diff --git a/auditbeat/module/file_integrity/fileinfo_posix.go b/auditbeat/module/file_integrity/fileinfo_posix.go index f70a638bc65a..d87c8fc4e20e 100644 --- a/auditbeat/module/file_integrity/fileinfo_posix.go +++ b/auditbeat/module/file_integrity/fileinfo_posix.go @@ -69,18 +69,7 @@ func NewMetadata(path string, info os.FileInfo) (*Metadata, error) { fileInfo.Owner = owner.Username } - var selinux []byte - getExtendedAttributes(path, map[string]*[]byte{ - "security.selinux": &selinux, - "system.posix_acl_access": &fileInfo.POSIXACLAccess, - }) - // The selinux attr may be null terminated. It would be cheaper - // to use strings.TrimRight, but absent documentation saying - // that there is only ever a final null terminator, take the - // guaranteed correct path of terminating at the first found - // null byte. - selinux, _, _ = bytes.Cut(selinux, []byte{0}) - fileInfo.SELinux = string(selinux) + fillExtendedAttributes(fileInfo, path) group, err := user.LookupGroupId(strconv.Itoa(int(fileInfo.GID))) if err != nil { @@ -91,9 +80,25 @@ func NewMetadata(path string, info os.FileInfo) (*Metadata, error) { if fileInfo.Origin, err = GetFileOrigin(path); err != nil { errs = append(errs, err) } + return fileInfo, errs.Err() } +func fillExtendedAttributes(md *Metadata, path string) { + var selinux []byte + getExtendedAttributes(path, map[string]*[]byte{ + "security.selinux": &selinux, + "system.posix_acl_access": &md.POSIXACLAccess, + }) + // The selinux attr may be null terminated. It would be cheaper + // to use strings.TrimRight, but absent documentation saying + // that there is only ever a final null terminator, take the + // guaranteed correct path of terminating at the first found + // null byte. + selinux, _, _ = bytes.Cut(selinux, []byte{0}) + md.SELinux = string(selinux) +} + func getExtendedAttributes(path string, dst map[string]*[]byte) { f, err := os.Open(path) if err != nil { diff --git a/auditbeat/module/file_integrity/flatbuffers.go b/auditbeat/module/file_integrity/flatbuffers.go index 837d39cf2262..f380e42252c3 100644 --- a/auditbeat/module/file_integrity/flatbuffers.go +++ b/auditbeat/module/file_integrity/flatbuffers.go @@ -164,6 +164,14 @@ func fbWriteMetadata(b *flatbuffers.Builder, m *Metadata) flatbuffers.UOffsetT { schema.MetadataAddType(b, schema.TypeDir) case SymlinkType: schema.MetadataAddType(b, schema.TypeSymlink) + case CharDeviceType: + schema.MetadataAddType(b, schema.TypeCharDevice) + case BlockDeviceType: + schema.MetadataAddType(b, schema.TypeBlockDevice) + case FIFOType: + schema.MetadataAddType(b, schema.TypeFIFO) + case SocketType: + schema.MetadataAddType(b, schema.TypeSocket) } if selinuxOffset > 0 { schema.MetadataAddSelinux(b, selinuxOffset) @@ -191,10 +199,12 @@ func fbWriteEvent(b *flatbuffers.Builder, e *Event) flatbuffers.UOffsetT { schema.EventAddTimestampNs(b, e.Timestamp.UnixNano()) switch e.Source { - case SourceFSNotify: - schema.EventAddSource(b, schema.SourceFSNotify) case SourceScan: schema.EventAddSource(b, schema.SourceScan) + case SourceFSNotify: + schema.EventAddSource(b, schema.SourceFSNotify) + case SourceEBPF: + schema.EventAddSource(b, schema.SourceEBPF) } if targetPathOffset > 0 { @@ -235,6 +245,8 @@ func fbDecodeEvent(path string, buf []byte) *Event { rtn.Source = SourceScan case schema.SourceFSNotify: rtn.Source = SourceFSNotify + case schema.SourceEBPF: + rtn.Source = SourceEBPF } action := e.Action() @@ -285,6 +297,14 @@ func fbDecodeMetadata(e *schema.Event) *Metadata { rtn.Type = DirType case schema.TypeSymlink: rtn.Type = SymlinkType + case schema.TypeCharDevice: + rtn.Type = CharDeviceType + case schema.TypeBlockDevice: + rtn.Type = BlockDeviceType + case schema.TypeFIFO: + rtn.Type = FIFOType + case schema.TypeSocket: + rtn.Type = SocketType default: rtn.Type = UnknownType } diff --git a/auditbeat/module/file_integrity/metricset.go b/auditbeat/module/file_integrity/metricset.go index 2c9c38d2d564..5ea1868ad57b 100644 --- a/auditbeat/module/file_integrity/metricset.go +++ b/auditbeat/module/file_integrity/metricset.go @@ -71,10 +71,10 @@ type MetricSet struct { log *logp.Logger // Runtime params that are initialized on Run(). - bucket datastore.BoltBucket - scanStart time.Time - scanChan <-chan Event - fsnotifyChan <-chan Event + bucket datastore.BoltBucket + scanStart time.Time + scanChan <-chan Event + eventChan <-chan Event // Used when a hash can't be calculated nullHashes map[HashType]Digest @@ -118,11 +118,11 @@ func (ms *MetricSet) Run(reporter mb.PushReporterV2) { return } - for ms.fsnotifyChan != nil || ms.scanChan != nil { + for ms.eventChan != nil || ms.scanChan != nil { select { - case event, ok := <-ms.fsnotifyChan: + case event, ok := <-ms.eventChan: if !ok { - ms.fsnotifyChan = nil + ms.eventChan = nil continue } @@ -161,9 +161,9 @@ func (ms *MetricSet) init(reporter mb.PushReporterV2) bool { } ms.bucket = bucket.(datastore.BoltBucket) - ms.fsnotifyChan, err = ms.reader.Start(reporter.Done()) + ms.eventChan, err = ms.reader.Start(reporter.Done()) if err != nil { - err = fmt.Errorf("failed to start fsnotify event producer: %w", err) + err = fmt.Errorf("failed to start event producer: %w", err) reporter.Error(err) ms.log.Errorw("Failed to initialize", "error", err) return false diff --git a/auditbeat/module/file_integrity/monitor/monitor.go b/auditbeat/module/file_integrity/monitor/monitor.go index 107a690d9754..ae80d1a17dc7 100644 --- a/auditbeat/module/file_integrity/monitor/monitor.go +++ b/auditbeat/module/file_integrity/monitor/monitor.go @@ -37,7 +37,7 @@ type Watcher interface { // New creates a new Watcher backed by fsnotify with optional recursive // logic. -func New(recursive bool, IsExcludedPath func(path string) bool) (Watcher, error) { +func New(recursive bool, isExcludedPath func(path string) bool) (Watcher, error) { watcher, err := fsnotify.NewWatcher() if err != nil { return nil, err @@ -45,7 +45,7 @@ func New(recursive bool, IsExcludedPath func(path string) bool) (Watcher, error) // Use our simulated recursive watches unless the fsnotify implementation // supports OS-provided recursive watches if recursive && watcher.SetRecursive() != nil { - return newRecursiveWatcher(watcher, IsExcludedPath), nil //nolint:nilerr // Ignore SetRecursive() errors. + return newRecursiveWatcher(watcher, isExcludedPath), nil //nolint:nilerr // Ignore SetRecursive() errors. } return (*nonRecursiveWatcher)(watcher), nil } diff --git a/auditbeat/module/file_integrity/monitor/recursive.go b/auditbeat/module/file_integrity/monitor/recursive.go index 80ab3e742ef3..7a0768d6fcbd 100644 --- a/auditbeat/module/file_integrity/monitor/recursive.go +++ b/auditbeat/module/file_integrity/monitor/recursive.go @@ -40,7 +40,7 @@ type recursiveWatcher struct { isExcludedPath func(path string) bool } -func newRecursiveWatcher(inner *fsnotify.Watcher, IsExcludedPath func(path string) bool) *recursiveWatcher { +func newRecursiveWatcher(inner *fsnotify.Watcher, isExcludedPath func(path string) bool) *recursiveWatcher { return &recursiveWatcher{ inner: inner, tree: FileTree{}, @@ -48,7 +48,7 @@ func newRecursiveWatcher(inner *fsnotify.Watcher, IsExcludedPath func(path strin addC: make(chan string), addErrC: make(chan error), log: logp.NewLogger(moduleName), - isExcludedPath: IsExcludedPath, + isExcludedPath: isExcludedPath, } } diff --git a/auditbeat/module/file_integrity/schema.fbs b/auditbeat/module/file_integrity/schema.fbs index 9e0863f6379a..1eaca3c7f6ca 100644 --- a/auditbeat/module/file_integrity/schema.fbs +++ b/auditbeat/module/file_integrity/schema.fbs @@ -12,6 +12,7 @@ enum Action : ubyte (bit_flags) { enum Source : ubyte { Scan, FSNotify, + EBPF, } enum Type : ubyte { @@ -19,6 +20,10 @@ enum Type : ubyte { File, Dir, Symlink, + CharDevice, + BlockDevice, + FIFO, + Socket, } table Metadata { diff --git a/auditbeat/module/file_integrity/schema/Source.go b/auditbeat/module/file_integrity/schema/Source.go index 94730ce29572..f3ac67cb9a5e 100644 --- a/auditbeat/module/file_integrity/schema/Source.go +++ b/auditbeat/module/file_integrity/schema/Source.go @@ -26,16 +26,19 @@ type Source byte const ( SourceScan Source = 0 SourceFSNotify Source = 1 + SourceEBPF Source = 2 ) var EnumNamesSource = map[Source]string{ SourceScan: "Scan", SourceFSNotify: "FSNotify", + SourceEBPF: "EBPF", } var EnumValuesSource = map[string]Source{ "Scan": SourceScan, "FSNotify": SourceFSNotify, + "EBPF": SourceEBPF, } func (v Source) String() string { diff --git a/auditbeat/module/file_integrity/schema/Type.go b/auditbeat/module/file_integrity/schema/Type.go index 2025ee3b0968..a0dc4e7a416e 100644 --- a/auditbeat/module/file_integrity/schema/Type.go +++ b/auditbeat/module/file_integrity/schema/Type.go @@ -24,24 +24,36 @@ import "strconv" type Type byte const ( - TypeUnknown Type = 0 - TypeFile Type = 1 - TypeDir Type = 2 - TypeSymlink Type = 3 + TypeUnknown Type = 0 + TypeFile Type = 1 + TypeDir Type = 2 + TypeSymlink Type = 3 + TypeCharDevice Type = 4 + TypeBlockDevice Type = 5 + TypeFIFO Type = 6 + TypeSocket Type = 7 ) var EnumNamesType = map[Type]string{ - TypeUnknown: "Unknown", - TypeFile: "File", - TypeDir: "Dir", - TypeSymlink: "Symlink", + TypeUnknown: "Unknown", + TypeFile: "File", + TypeDir: "Dir", + TypeSymlink: "Symlink", + TypeCharDevice: "CharDevice", + TypeBlockDevice: "BlockDevice", + TypeFIFO: "FIFO", + TypeSocket: "Socket", } var EnumValuesType = map[string]Type{ - "Unknown": TypeUnknown, - "File": TypeFile, - "Dir": TypeDir, - "Symlink": TypeSymlink, + "Unknown": TypeUnknown, + "File": TypeFile, + "Dir": TypeDir, + "Symlink": TypeSymlink, + "CharDevice": TypeCharDevice, + "BlockDevice": TypeBlockDevice, + "FIFO": TypeFIFO, + "Socket": TypeSocket, } func (v Type) String() string { diff --git a/auditbeat/tests/system/test_file_integrity.py b/auditbeat/tests/system/test_file_integrity.py index 280d2916a550..55c34266c469 100644 --- a/auditbeat/tests/system/test_file_integrity.py +++ b/auditbeat/tests/system/test_file_integrity.py @@ -1,9 +1,16 @@ +import os import time import unittest import platform from auditbeat import * +def is_root(): + if 'geteuid' not in dir(os): + return False + return os.geteuid() == 0 + + # Escapes a path to match what's printed in the logs def escape_path(path): return path.replace('\\', '\\\\') @@ -16,7 +23,8 @@ def has_file(objs, path, sha1hash): and obj['file.path'].lower() == path.lower() and obj['file.hash.sha1'] == sha1hash: found = True break - assert found, "File '{0}' with sha1sum '{1}' not found".format(path, sha1hash) + assert found, "File '{0}' with sha1sum '{1}' not found".format( + path, sha1hash) def has_dir(objs, path): @@ -49,9 +57,9 @@ 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)) + 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 prev_lines = -1 while True: @@ -62,9 +70,17 @@ def wait_output(self, min_events): 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 == "ebpf": + self.wait_log_contains("started ebpf 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 +89,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 +124,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": + 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 +143,8 @@ def test_non_recursive(self): has_file(objs, file1, "430ce34d020724ed75a196dfc2ad67c77772d169") has_file(objs, file2, "d23be250530a24be33069572db67995f21244c51") - has_dir(objs, subdir) + if backend == "fsnotify": + has_dir(objs, subdir) file_events(objs, file1, ['created', 'deleted']) file_events(objs, file2, ['created']) @@ -134,8 +152,16 @@ 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_root(), "Requires root") + def test_non_recursive__ebpf(self): + self._test_non_recursive("ebpf") + + def _test_recursive(self, backend): """ file_integrity monitors watched directories (recursive). """ @@ -143,22 +169,22 @@ 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 +200,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": + 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 +220,83 @@ 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": + 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_root(), "Requires root") + def test_recursive__ebpf(self): + self._test_recursive("ebpf") + + + @unittest.skipIf(platform.system() != 'Linux', 'Non linux, skipping.') + 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!") + + # FSNotify can't catch the events if operations happens too fast + time.sleep(1) + + # Event 2: chmod + os.chmod(f, 0o777) + # 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") + # FSNotify can't catch the events if operations happens too fast + time.sleep(1) + + # Event 4: truncate + fd.truncate(0) + # 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.skipIf(platform.system() != 'Linux', 'Non linux, skipping.') + @unittest.skipUnless(is_root(), "Requires root") + def test_file_modified__ebpf(self): + self._test_file_modified("ebpf") diff --git a/dev-tools/notice/overrides.json b/dev-tools/notice/overrides.json index 1484fcde52a0..eee18acc0de5 100644 --- a/dev-tools/notice/overrides.json +++ b/dev-tools/notice/overrides.json @@ -17,3 +17,4 @@ {"name": "github.com/awslabs/kinesis-aggregation/go/v2", "licenceType": "Apache-2.0", "url": "https://github.com/awslabs/kinesis-aggregation/blob/master/LICENSE.txt"} {"name": "github.com/dnaeon/go-vcr", "licenceType": "BSD-2-Clause"} {"name": "github.com/JohnCGriffin/overflow", "licenceType": "MIT"} +{"name": "github.com/elastic/ebpfevents", "licenceType": "Apache-2.0"} diff --git a/go.mod b/go.mod index e39b37f445eb..68c10aafaa63 100644 --- a/go.mod +++ b/go.mod @@ -200,6 +200,7 @@ require ( github.com/aws/smithy-go v1.13.5 github.com/awslabs/kinesis-aggregation/go/v2 v2.0.0-20220623125934-28468a6701b5 github.com/elastic/bayeux v1.0.5 + github.com/elastic/ebpfevents v0.3.3-0.20240131115527-3312e650a303 github.com/elastic/elastic-agent-autodiscover v0.6.7 github.com/elastic/elastic-agent-libs v0.7.5 github.com/elastic/elastic-agent-shipper-client v0.5.1-0.20230228231646-f04347b666f3 @@ -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 e11edad3d976..7e108b5c34fd 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= @@ -657,6 +659,8 @@ github.com/elastic/bayeux v1.0.5 h1:UceFq01ipmT3S8DzFK+uVAkbCdiPR0Bqei8qIGmUeY0= github.com/elastic/bayeux v1.0.5/go.mod h1:CSI4iP7qeo5MMlkznGvYKftp8M7qqP/3nzmVZoXHY68= github.com/elastic/dhcp v0.0.0-20200227161230-57ec251c7eb3 h1:lnDkqiRFKm0rxdljqrj3lotWinO9+jFmeDXIC4gvIQs= github.com/elastic/dhcp v0.0.0-20200227161230-57ec251c7eb3/go.mod h1:aPqzac6AYkipvp4hufTyMj5PDIphF3+At8zr7r51xjY= +github.com/elastic/ebpfevents v0.3.3-0.20240131115527-3312e650a303 h1:tetql7mXwuEJ7+Tpc0ZwZbE5gLe41T7Cimmgo5jbqI4= +github.com/elastic/ebpfevents v0.3.3-0.20240131115527-3312e650a303/go.mod h1:o21z5xup/9dK8u0Hg9bZRflSqqj1Zu5h2dg2hSTcUPQ= github.com/elastic/elastic-agent-autodiscover v0.6.7 h1:+KVjltN0rPsBrU8b156gV4lOTBgG/vt0efFCFARrf3g= github.com/elastic/elastic-agent-autodiscover v0.6.7/go.mod h1:hFeFqneS2r4jD0/QzGkrNk0YVdN0JGh7lCWdsH7zcI4= github.com/elastic/elastic-agent-client/v7 v7.6.0 h1:FEn6FjzynW4TIQo5G096Tr7xYK/P5LY9cSS6wRbXZTc= @@ -754,8 +758,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= @@ -767,6 +771,8 @@ github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0 github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= github.com/go-chi/chi v4.1.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-faker/faker/v4 v4.2.0 h1:dGebOupKwssrODV51E0zbMrv5e2gO9VWSLNC1WDCpWg= +github.com/go-faker/faker/v4 v4.2.0/go.mod h1:F/bBy8GH9NxOxMInug5Gx4WYeG6fHJZ8Ol/dhcpRub4= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= @@ -1340,8 +1346,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= diff --git a/x-pack/auditbeat/auditbeat.reference.yml b/x-pack/auditbeat/auditbeat.reference.yml index 45d1c4af8510..fd6ab961be3d 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, 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.