diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b19dfef --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,63 @@ +name: Release Workflow + +on: + push: + branches: + - main + pull_request: + branches: + - main + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + + - name: Install GoReleaser + run: go install github.com/goreleaser/goreleaser@latest + + - name: Check version + run: | + VERSION=$(cat VERSION.txt) + echo "version=$VERSION" >> $GITHUB_ENV + echo "Version: $VERSION" + + - name: Run build using goreleaser on local + run: goreleaser release --snapshot --skip=publish --clean + + - name: Create Tag + if: | + (github.event_name == 'push' && github.ref == 'refs/heads/main') || + (github.event_name == 'pull_request' && + github.event.action == 'closed' && + github.event.pull_request.merged == true && + github.event.pull_request.base.ref == 'main') + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git tag "v${{ env.version }}" + git push origin "v${{ env.version }}" + + - name: Run GoReleaser Release + if: | + (github.event_name == 'push' && github.ref == 'refs/heads/main') || + (github.event_name == 'pull_request' && + github.event.action == 'closed' && + github.event.pull_request.merged == true && + github.event.pull_request.base.ref == 'main') + run: | + export GORELEASER_CURRENT_TAG="v${{ env.version }}" + export GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} + goreleaser release --clean --rm-dist + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 6f72f89..0742e7f 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ go.work.sum # env file .env + +# build +dist \ No newline at end of file diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..a8dc7c3 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,20 @@ +project_name: go-ebpf-logger + +builds: + - main: ./main.go + goos: + - linux + - darwin + - windows + goarch: + - amd64 + - arm64 + +archives: + - format: tar.gz + name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' + +release: + github: + owner: siddh34 + name: go-ebpf-logger \ No newline at end of file diff --git a/Dockerfile.amd64 b/Dockerfile.amd64 new file mode 100644 index 0000000..c3fffea --- /dev/null +++ b/Dockerfile.amd64 @@ -0,0 +1,13 @@ +FROM debian:stable-slim + +ARG TARGETARCH + +WORKDIR /app + +COPY dist/go-ebpf-logger_linux_${TARGETARCH}_v1/go-ebpf-logger /app/go-ebpf-logger + +COPY monitor.bpf.o /app/monitor.bpf.o + +RUN chmod +x /app/go-ebpf-logger + +ENTRYPOINT ["/app/go-ebpf-logger"] \ No newline at end of file diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 new file mode 100644 index 0000000..4ba1001 --- /dev/null +++ b/Dockerfile.arm64 @@ -0,0 +1,13 @@ +FROM debian:stable-slim + +ARG TARGETARCH + +WORKDIR /app + +COPY dist/go-ebpf-logger_linux_${TARGETARCH}/go-ebpf-logger /app/go-ebpf-logger + +COPY monitor.bpf.o /app/monitor.bpf.o + +RUN chmod +x /app/go-ebpf-logger + +ENTRYPOINT ["/app/go-ebpf-logger"] \ No newline at end of file diff --git a/README.md b/README.md index 2cdce00..da6ba32 100644 --- a/README.md +++ b/README.md @@ -1 +1,62 @@ -# go-ebpf-logger \ No newline at end of file +# go-ebpf-logger + +The golang code that actually runs the file monitor ebpf code + +## Pre-requisite + +Install golang, visit [link](https://go.dev/doc/install) + +Install goreleaser, visit [link](https://goreleaser.com/install/#aur) + +Install gh, visit [link](https://cli.github.com/) + +## Screenshots + +It works! + +![Screenshot showing go-ebpf-logger in action](./docs/assets/working.png) + + +## Run it on Local + +Get file monitor binary from the repo + +Put the version accordingly here in the below command at `vX.Y.Z` + +```sh +gh release download vX.Y.Z --repo SentinalFS/file-monitor --pattern "monitor.bpf.o" +``` + +Run it + +```sh +sudo go run main.go +``` + +## Run it on docker + +Get file monitor binary from the repo + +Put the version accordingly here in the below command at `vX.Y.Z` + +```sh +gh release download vX.Y.Z --repo SentinalFS/file-monitor --pattern "monitor.bpf.o" +``` + +Run go releaser on local + +```sh +goreleaser release --snapshot --skip=publish --clean +``` + +Build it + +```sh +docker build --build-arg TARGETARCH=amd64 -t go-ebpf-logger -f Dockerfile.amd64 . +``` + +Run it + +```sh +sudo docker run --rm -it --privileged -v /sys/fs/bpf:/sys/fs/bpf:rw go-ebpf-logger +``` \ No newline at end of file diff --git a/VERSION.txt b/VERSION.txt new file mode 100644 index 0000000..8a9ecc2 --- /dev/null +++ b/VERSION.txt @@ -0,0 +1 @@ +0.0.1 \ No newline at end of file diff --git a/bpf/pin_maps.go b/bpf/pin_maps.go new file mode 100644 index 0000000..c39a5fb --- /dev/null +++ b/bpf/pin_maps.go @@ -0,0 +1,28 @@ +package bpf + +import ( + "os" + + "github.com/cilium/ebpf" + + "fmt" + + "go-ebp-logger/constants" +) + +func pinMaps(m *ebpf.Map) error { + if m != nil { + path := "/sys/fs/bpf/" + constants.InodeMapName + if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + fmt.Printf("Warning: failed to remove existing pin at %s: %v", path, err) + } + if err := m.Pin(path); err != nil { + fmt.Printf("Failed to pin map %s to %s: %v", constants.InodeMapName,path, err) + } + fmt.Printf("Map %s pinned succesfully", constants.InodeMapName) + return nil + } + + fmt.Printf("Map %s not found", constants.InodeMapName) + return fmt.Errorf("no map was pinned") +} \ No newline at end of file diff --git a/bpf/setup.go b/bpf/setup.go new file mode 100644 index 0000000..04b80ed --- /dev/null +++ b/bpf/setup.go @@ -0,0 +1,60 @@ +package bpf + +import ( + "fmt" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/link" + + "go-ebp-logger/constants" +) + +func SetupBPF(bpfObj string) (*ebpf.Map, *ebpf.Map, func()) { + spec, err := ebpf.LoadCollectionSpec(bpfObj) + if err != nil { + fmt.Printf("Failed to load BPF collection spec: %v\n", err) + } + + coll, err := ebpf.NewCollection(spec) + if err != nil { + fmt.Printf("Failed to load BPF collection: %v\n", err) + } + + var links []*link.Link + for progName, fn := range constants.ProgsToFuncs { + prog := coll.Programs[progName] + if prog == nil { + fmt.Printf("Program '%s' not found\n", progName) + } + kp, err := link.Kprobe(fn, prog, nil) + if err != nil { + fmt.Printf("Failed to attach kprobe to %s: %v\n", fn, err) + } + links = append(links, &kp) + } + + monitored_inode_map := coll.Maps["monitored_inodes"] + err = pinMaps(monitored_inode_map) + if err != nil { + panic(err) + } + + events := coll.Maps["events"] + if events == nil { + fmt.Printf("Map 'events' not found\n") + } + + renameEvents := coll.Maps["rename_events"] + if renameEvents == nil { + fmt.Printf("Map 'rename_events' not found\n") + } + + cleanup := func() { + for _, l := range links { + (*l).Close() + } + coll.Close() + } + + return events, renameEvents, cleanup +} \ No newline at end of file diff --git a/constants/bpf.go b/constants/bpf.go new file mode 100644 index 0000000..b601cca --- /dev/null +++ b/constants/bpf.go @@ -0,0 +1,13 @@ +package constants + + +var ProgsToFuncs = map[string]string{ + "trace_read": "vfs_read", + "trace_write": "vfs_write", + "trace_rename": "vfs_rename", + "trace_delete": "vfs_unlink", +} + +var InodeMapName = "monitored_inodes" + +var BpfObjName = "monitor.bpf.o" \ No newline at end of file diff --git a/docs/assets/working.png b/docs/assets/working.png new file mode 100644 index 0000000..cb594bb Binary files /dev/null and b/docs/assets/working.png differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..70a7381 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module go-ebp-logger + +go 1.20 + +require github.com/cilium/ebpf v0.12.0 + +require ( + golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 // indirect + golang.org/x/sys v0.6.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..00298ef --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +github.com/cilium/ebpf v0.12.0 h1:oQEuIQIXgYhe1v7sYUG0P9vtJTYZLLdA6tiQmrOB1mo= +github.com/cilium/ebpf v0.12.0/go.mod h1:u9H29/Iq+8cy70YqI6p5pfADkFl3vdnV2qXDg5JL0Zo= +github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +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/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/main.go b/main.go new file mode 100644 index 0000000..91c4267 --- /dev/null +++ b/main.go @@ -0,0 +1,74 @@ +package main + +import ( + "go-ebp-logger/bpf" + "go-ebp-logger/constants" + "go-ebp-logger/utils" + "os" + "os/signal" + "syscall" + + "github.com/cilium/ebpf/ringbuf" + + "fmt" +) + +func main() { + events, renameEvents, cleanup := bpf.SetupBPF(constants.BpfObjName) + defer cleanup() + + baseEventsRB, err := ringbuf.NewReader(events) + if err != nil { + fmt.Printf("Failed to create ring buffer reader: %v", err) + panic(err) + } + defer baseEventsRB.Close() + + renameEventsRB, err := ringbuf.NewReader(renameEvents) + if err != nil { + fmt.Printf("Failed to create ring buffer reader: %v", err) + panic(err) + } + defer renameEventsRB.Close() + + done := make(chan struct{}) + fmt.Println("Waiting for events... Press Ctrl+C to stop.") + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + + go func() { + for { + select { + case <-done: + return + default: + record, err := baseEventsRB.Read() + if err != nil { + fmt.Printf("Failed to read from ring buffer 1: %v", err) + continue + } + utils.PrintBaseEvent(record.RawSample) + } + } + }() + + go func() { + for { + select { + case <-done: + return + default: + record, err := renameEventsRB.Read() + if err != nil { + fmt.Printf("Failed to read from ring buffer 1: %v", err) + continue + } + utils.PrintRenameEvent(record.RawSample) + } + } + }() + + <-sigCh + close(done) + fmt.Println("\nExiting...") +} diff --git a/models/models.go b/models/models.go new file mode 100644 index 0000000..a9b7c9a --- /dev/null +++ b/models/models.go @@ -0,0 +1,22 @@ +package models + +type BaseEventData struct { + Pid uint32 + Uid uint32 + Filename [144]byte + Comm [32]byte + Timestamp uint64 + CgroupId uint64 + Otype [16]byte +} + +type RenameData struct { + Pid uint32 + Uid uint32 + OldFileName [144]byte + NewFileName [144]byte + Comm [32]byte + Timestamp uint64 + CgroupId uint64 + Otype [16]byte +} \ No newline at end of file diff --git a/monitor.bpf.o b/monitor.bpf.o new file mode 100644 index 0000000..a7367ce Binary files /dev/null and b/monitor.bpf.o differ diff --git a/utils/events.go b/utils/events.go new file mode 100644 index 0000000..4a05214 --- /dev/null +++ b/utils/events.go @@ -0,0 +1,49 @@ +package utils + +import ( + "bytes" + "encoding/binary" + "fmt" + "strings" + "time" + + "go-ebp-logger/models" +) + + + +func PrintBaseEvent(data []byte) { + var event models.BaseEventData + if err := binary.Read(bytes.NewReader(data), binary.LittleEndian, &event); err != nil { + fmt.Printf("Failed to decode event: %v\n", err) + return + } + ts := time.Unix(0, int64(event.Timestamp)) + fmt.Printf("[%s] PID=%d, UID=%d, CgroupId=%d, Operation=%s, File=%s, Process=%s\n", + ts.Format("2006-01-02 15:04:05.000"), + event.Pid, + event.Uid, + event.CgroupId, + strings.TrimRight(string(event.Otype[:]), "\x00"), + strings.TrimRight(string(event.Filename[:]), "\x00"), + strings.TrimRight(string(event.Comm[:]), "\x00"), + ) +} + +func PrintRenameEvent(data []byte) { + var event models.RenameData + if err := binary.Read(bytes.NewReader(data), binary.LittleEndian, &event); err != nil { + fmt.Printf("Failed to decode rename event: %v\n", err) + return + } + ts := time.Unix(0, int64(event.Timestamp)) + fmt.Printf("[%s] PID=%d, UID=%d, CgroupId=%d, OldFile=%s, NewFile=%s, Process=%s\n", + ts.Format("2006-01-02 15:04:05.000"), + event.Pid, + event.Uid, + event.CgroupId, + strings.TrimRight(string(event.OldFileName[:]), "\x00"), + strings.TrimRight(string(event.NewFileName[:]), "\x00"), + strings.TrimRight(string(event.Comm[:]), "\x00"), + ) +} \ No newline at end of file