From df74d2c649a4b703ed47169ebd2526d83f62492b 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 auditbeat/.gitignore | 1 - auditbeat/auditbeat.reference.yml | 6 +- auditbeat/docker-compose.yml | 1 + auditbeat/internal/ebpf/seccomp_linux.go | 40 ++++ auditbeat/internal/ebpf/watcher_linux.go | 175 ++++++++++++++++++ auditbeat/internal/ebpf/watcher_other.go | 28 +++ auditbeat/internal/ebpf/watcher_test.go | 63 +++++++ auditbeat/module/file_integrity/config.go | 20 ++ auditbeat/module/file_integrity/event.go | 123 +++++++++--- .../module/file_integrity/eventreader_ebpf.go | 117 ++++++++++++ .../file_integrity/eventreader_fsevents.go | 8 +- .../file_integrity/eventreader_fsnotify.go | 19 +- .../file_integrity/eventreader_linux.go | 55 ++++++ .../file_integrity/eventreader_other.go | 34 ++++ .../module/file_integrity/fileinfo_ebpf.go | 101 ++++++++++ .../module/file_integrity/fileinfo_posix.go | 29 +-- auditbeat/module/file_integrity/metricset.go | 18 +- .../module/file_integrity/monitor/monitor.go | 4 +- .../file_integrity/monitor/recursive.go | 4 +- go.mod | 6 +- go.sum | 16 +- 22 files changed, 789 insertions(+), 79 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/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/eventreader_ebpf.go create mode 100644 auditbeat/module/file_integrity/eventreader_linux.go create mode 100644 auditbeat/module/file_integrity/eventreader_other.go create mode 100644 auditbeat/module/file_integrity/fileinfo_ebpf.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/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 a3a36dde753f..35e64cdd3170 100644 --- a/auditbeat/auditbeat.reference.yml +++ b/auditbeat/auditbeat.reference.yml @@ -116,6 +116,11 @@ auditbeat.modules: # Set to true to publish fields with null values in events. #keep_null: false + # Select the backend which will be used to source events. + # Valid values: ebpf, fsnotify. + # Default: fsnotify. + force_backend: fsnotify + # Parse detailed information for the listed fields. Field paths in the list below # that are a prefix of other field paths imply the longer field path. A set of # fields may be specified using an RE2 regular expression quoted in //. For example @@ -1785,4 +1790,3 @@ logging.files: #features: # fqdn: # enabled: true - 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/internal/ebpf/seccomp_linux.go b/auditbeat/internal/ebpf/seccomp_linux.go new file mode 100644 index 000000000000..9eb76db1fe2d --- /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", "arm64": + 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/watcher_linux.go b/auditbeat/internal/ebpf/watcher_linux.go new file mode 100644 index 000000000000..aa1e96d99318 --- /dev/null +++ b/auditbeat/internal/ebpf/watcher_linux.go @@ -0,0 +1,175 @@ +// 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" +) + +func init() { + gWatcher = &watcher{} +} + +type EventMask uint64 + +type Watcher interface { + Subscribe(string, EventMask) (<-chan ebpfevents.Event, <-chan error) + Unsubscribe(string) +} + +var gWatcher *watcher + +type client struct { + name string + mask EventMask + events chan ebpfevents.Event + errors chan error +} + +type watcher struct { + sync.Mutex + ctx context.Context + cancel context.CancelFunc + loader *ebpfevents.Loader + clients map[string]client + status status + err error +} + +type status int + +const ( + stopped status = iota + started +) + +func GetWatcher() (Watcher, error) { + gWatcher.Lock() + defer gWatcher.Unlock() + + if gWatcher.err != nil { + return nil, gWatcher.err + } + + if gWatcher.status == stopped { + startLocked() + } + + return gWatcher, gWatcher.err +} + +func (w *watcher) Subscribe(name string, events EventMask) (<-chan ebpfevents.Event, <-chan error) { + w.Lock() + defer w.Unlock() + + if w.status == stopped { + startLocked() + } + + w.clients[name] = client{ + name: name, + mask: events, + events: make(chan ebpfevents.Event), + errors: make(chan error), + } + + return w.clients[name].events, w.clients[name].errors +} + +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 { + gWatcher.err = fmt.Errorf("new ebpf loader: %w", err) + return + } + + gWatcher.loader = loader + gWatcher.clients = make(map[string]client) + + events := make(chan ebpfevents.Event) + errors := make(chan error) + gWatcher.ctx, gWatcher.cancel = context.WithCancel(context.Background()) + + go gWatcher.loader.EventLoop(gWatcher.ctx, events, errors) + go func() { + for { + select { + case err := <-errors: + for _, client := range gWatcher.clients { + client.errors <- err + } + continue + case ev := <-events: + for _, client := range gWatcher.clients { + if client.mask&EventMask(ev.Type) != 0 { + client.events <- ev + } + } + continue + case <-gWatcher.ctx.Done(): + return + } + } + }() + + gWatcher.status = started +} + +func stopLocked() { + _ = gWatcher.close() + gWatcher.status = stopped + gWatcher.err = nil +} + +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.events) + close(cl.errors) + } + + 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..0150ac1ad62f --- /dev/null +++ b/auditbeat/internal/ebpf/watcher_test.go @@ -0,0 +1,63 @@ +// 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) { + assert.Equal(t, gWatcher.status, stopped) + assert.Nil(t, gWatcher.err) + + w, err := GetWatcher() + if err != nil { + t.Skipf("skipping ebpf watcher test: %v", err) + } + assert.Equal(t, gWatcher.status, started) + assert.Equal(t, 0, gWatcher.nclients()) + + _, _ = w.Subscribe("test-1", allEvents) + 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/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..195fcaeda5a2 100644 --- a/auditbeat/module/file_integrity/event.go +++ b/auditbeat/module/file_integrity/event.go @@ -43,6 +43,7 @@ import ( "github.com/elastic/beats/v7/libbeat/common/file" "github.com/elastic/beats/v7/metricbeat/mb" + "github.com/elastic/ebpfevents" "github.com/elastic/elastic-agent-libs/mapstr" ) @@ -65,11 +66,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 +95,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,29 +201,7 @@ 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) } @@ -219,6 +209,32 @@ func NewEventFromFileInfo( 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 { + err = p.Parse(event.ParserResults, path) + if 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( @@ -241,6 +257,61 @@ func NewEvent( return NewEventFromFileInfo(path, info, err, action, source, maxFileSize, hashTypes, fileParsers) } +// NewEventFromEbpfEvent creates a new Event from an ebpfevents.Event. +func NewEventFromEbpfEvent( + ee ebpfevents.Event, + maxFileSize uint64, + hashTypes []HashType, + fileParsers []FileParser, +) Event { + var ( + path, target string + action Action + metadata Metadata + ) + switch ee.Type { + case ebpfevents.EventTypeFileCreate: + action = Created + + fileCreateEvent := ee.Body.(*ebpfevents.FileCreate) + path = fileCreateEvent.Path + target = fileCreateEvent.SymlinkTargetPath + metadata = metadataFromFileCreate(fileCreateEvent) + case ebpfevents.EventTypeFileRename: + action = Updated + + fileRenameEvent := ee.Body.(*ebpfevents.FileRename) + path = fileRenameEvent.NewPath + target = fileRenameEvent.SymlinkTargetPath + metadata = metadataFromFileRename(fileRenameEvent) + case ebpfevents.EventTypeFileDelete: + action = Deleted + + fileDeleteEvent := ee.Body.(*ebpfevents.FileDelete) + path = fileDeleteEvent.Path + target = fileDeleteEvent.SymlinkTargetPath + metadata = metadataFromFileDelete(fileDeleteEvent) + } + + event := Event{ + Timestamp: time.Now().UTC(), + Path: path, + TargetPath: target, + Info: &metadata, + Source: SourceEBPF, + Action: action, + } + + switch event.Info.Type { + case FileType: + fillHashes(&event, path, maxFileSize, hashTypes, fileParsers) + case SymlinkType: + event.TargetPath, _ = filepath.EvalSymlinks(event.Path) + } + + return event +} + func isASCIILetter(letter byte) bool { // It appears that Windows only allows ascii characters for drive letters // and that's what go checks for: https://golang.org/src/path/filepath/path_windows.go#L63 diff --git a/auditbeat/module/file_integrity/eventreader_ebpf.go b/auditbeat/module/file_integrity/eventreader_ebpf.go new file mode 100644 index 000000000000..2a732fcdf1fa --- /dev/null +++ b/auditbeat/module/file_integrity/eventreader_ebpf.go @@ -0,0 +1,117 @@ +// 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{} + + _events <-chan ebpfevents.Event + _errors <-chan error +} + +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) + r._events, r._errors = r.watcher.Subscribe(clientName, mask) + + go r.consumeEvents() + + r.log.Info("started ebpf watcher") + return r.eventC, nil +} + +func (r *ebpfReader) consumeEvents() { + defer close(r.eventC) + defer r.watcher.Unsubscribe(clientName) + + for { + select { + case event := <-r._events: + r.log.Debugf("received ebpf event: %v", event) + + if event.Type != ebpfevents.EventTypeFileCreate && + event.Type != ebpfevents.EventTypeFileRename && + event.Type != ebpfevents.EventTypeFileDelete { + r.log.Warnf("received unwanted ebpf event: %s", event.Type.String()) + } + + start := time.Now() + e := NewEventFromEbpfEvent(event, r.config.MaxFileSizeBytes, r.config.HashTypes, r.parsers) + e.rtt = time.Since(start) + + if r.excludedPath(e.Path) { + continue + } + + r.eventC <- e + case err := <-r._errors: + r.log.Errorf("ebpf watcher error: %v", err) + case <-r.done: + r.log.Debug("ebpf watcher terminated") + return + } + } +} + +func (r *ebpfReader) excludedPath(path string) bool { + dir := filepath.Dir(path) + + 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(p, dir) { + 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..842f6d5ffd6c 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,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 { @@ -115,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() @@ -129,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 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_ebpf.go b/auditbeat/module/file_integrity/fileinfo_ebpf.go new file mode 100644 index 000000000000..bd54fb5f0105 --- /dev/null +++ b/auditbeat/module/file_integrity/fileinfo_ebpf.go @@ -0,0 +1,101 @@ +// 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" + "strconv" + + "github.com/elastic/ebpfevents" +) + +func metadataFromFileCreate(evt *ebpfevents.FileCreate) Metadata { + var md Metadata + fillFileInfo(&md, evt.Finfo) + fillExtendedAttributes(&md, evt.Path) + return md +} + +func metadataFromFileRename(evt *ebpfevents.FileRename) Metadata { + var md Metadata + fillFileInfo(&md, evt.Finfo) + fillExtendedAttributes(&md, evt.NewPath) + return md +} + +func metadataFromFileDelete(evt *ebpfevents.FileDelete) Metadata { + var md Metadata + fillFileInfo(&md, evt.Finfo) + fillExtendedAttributes(&md, evt.Path) + return md +} + +func fillFileInfo(md *Metadata, finfo ebpfevents.FileInfo) { + var owner, group string + + u, err := user.LookupId(strconv.FormatUint(uint64(finfo.Uid), 10)) + if err != nil { + owner = "n/a" + } else { + owner = u.Username + } + + g, err := user.LookupGroupId(strconv.FormatUint(uint64(finfo.Gid), 10)) + if err != nil { + group = "n/a" + } else { + group = g.Name + } + + md.Inode = finfo.Inode + md.UID = finfo.Uid + md.GID = finfo.Gid + md.Owner = owner + md.Group = group + 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 +} + +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/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/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/go.mod b/go.mod index 20e3c4c43e14..2a6aa29a1c21 100644 --- a/go.mod +++ b/go.mod @@ -158,7 +158,7 @@ require ( golang.org/x/net v0.17.0 golang.org/x/oauth2 v0.10.0 golang.org/x/sync v0.3.0 - golang.org/x/sys v0.13.0 + golang.org/x/sys v0.14.1-0.20231108175955-e4099bfacb8c golang.org/x/text v0.13.0 golang.org/x/time v0.3.0 golang.org/x/tools v0.9.1 @@ -201,6 +201,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.0.0-20231116123029-c0d511faa878 github.com/elastic/elastic-agent-autodiscover v0.6.4 github.com/elastic/elastic-agent-libs v0.6.2 github.com/elastic/elastic-agent-shipper-client v0.5.1-0.20230228231646-f04347b666f3 @@ -263,6 +264,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 @@ -364,7 +366,7 @@ require ( go.opentelemetry.io/otel v1.19.0 // indirect go.opentelemetry.io/otel/metric v1.19.0 // indirect go.opentelemetry.io/otel/trace v1.19.0 // indirect - golang.org/x/exp v0.0.0-20220921023135-46d9e7742f1e // indirect + golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 // indirect golang.org/x/term v0.13.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/go.sum b/go.sum index 58c3c2af8382..7a537b6b2099 100644 --- a/go.sum +++ b/go.sum @@ -423,6 +423,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= @@ -650,6 +652,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.0.0-20231116123029-c0d511faa878 h1:1MqMh1+HFadsgrnrKxwRGA9XJvjqWt+rDdjUEqvuhpI= +github.com/elastic/ebpfevents v0.0.0-20231116123029-c0d511faa878/go.mod h1:QSLUSc/YUrZYnIPbmAqPwubIknvybbMemBHC9uudMTc= github.com/elastic/elastic-agent-autodiscover v0.6.4 h1:K+xC7OGgcy4fLXVuGgOGLs+eXCqRnRg2SQQinxP+KsA= github.com/elastic/elastic-agent-autodiscover v0.6.4/go.mod h1:5+7NIBAILc0GkgxYW3ckXncu5wRZfltZhTY4aZAYP4M= github.com/elastic/elastic-agent-client/v7 v7.4.0 h1:h75oTkkvIjgiKVm61NpvTZP4cy6QbQ3zrIpXKGigyjo= @@ -748,7 +752,7 @@ 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.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= 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= @@ -1327,8 +1331,8 @@ 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/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= @@ -2045,8 +2049,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/exp v0.0.0-20220921023135-46d9e7742f1e h1:Ctm9yurWsg7aWwIpH9Bnap/IdSVxixymIb3MhiMEQQA= -golang.org/x/exp v0.0.0-20220921023135-46d9e7742f1e/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 h1:Jvc7gsqn21cJHCmAWx0LiimpP18LZmUxkT5Mp7EZ1mI= +golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -2342,8 +2346,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.1-0.20231108175955-e4099bfacb8c h1:3kC/TjQ+xzIblQv39bCOyRk8fbEeJcDHwbyxPUU2BpA= +golang.org/x/sys v0.14.1-0.20231108175955-e4099bfacb8c/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=