Skip to content

Commit

Permalink
fim: implement ebpf backend
Browse files Browse the repository at this point in the history
  • Loading branch information
mmat11 committed Dec 4, 2023
1 parent 0baba39 commit ebac402
Show file tree
Hide file tree
Showing 33 changed files with 1,353 additions and 206 deletions.
File renamed without changes.
292 changes: 284 additions & 8 deletions NOTICE.txt

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion auditbeat/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,3 @@ module/*/_meta/config.yml
/auditbeat
/auditbeat.test
/docs/html_docs

4 changes: 4 additions & 0 deletions auditbeat/auditbeat.reference.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions auditbeat/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion auditbeat/docs/modules/file_integrity.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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`, `inotify`. Default: `inotify`.

include::{docdir}/auditbeat-options.asciidoc[]


Expand Down
40 changes: 40 additions & 0 deletions auditbeat/internal/ebpf/seccomp_linux.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
27 changes: 27 additions & 0 deletions auditbeat/internal/ebpf/types.go
Original file line number Diff line number Diff line change
@@ -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.Event, <-chan error)
Unsubscribe(string)
}
170 changes: 170 additions & 0 deletions auditbeat/internal/ebpf/watcher_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// 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
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
}

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.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 {
gWatcherErr = fmt.Errorf("start 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
}

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
}
28 changes: 28 additions & 0 deletions auditbeat/internal/ebpf/watcher_other.go
Original file line number Diff line number Diff line change
@@ -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
}
61 changes: 61 additions & 0 deletions auditbeat/internal/ebpf/watcher_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
7 changes: 7 additions & 0 deletions auditbeat/module/file_integrity/_meta/config.yml.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit ebac402

Please sign in to comment.