feat(go): advance validation sidecar toward production

This commit is contained in:
Luna 2026-07-22 01:52:19 -07:00
parent 6a06eba255
commit f85c2e831a
No known key found for this signature in database
92 changed files with 8881 additions and 91 deletions

View file

@ -0,0 +1,84 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#define SEC(name) __attribute__((section(name), used))
#define __uint(name, value) int (*name)[value]
typedef unsigned int u32;
typedef unsigned long long u64;
#define BPF_MAP_TYPE_PERF_EVENT_ARRAY 4
#define BPF_F_CURRENT_CPU 0xffffffffULL
struct trace_event_raw_sys_enter {
u64 _common;
long id;
unsigned long args[6];
};
// Minimal CO-RE flavor of task_struct. The loader relocates these two fields
// against the running kernel's BTF, avoiding a generated vmlinux.h dependency.
struct task_struct {
int tgid;
struct task_struct *real_parent;
} __attribute__((preserve_access_index));
struct exec_event {
u32 pid;
u32 ppid;
u32 uid;
char parent_comm[16];
char filename[128];
char arg1[128];
char arg2[128];
};
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(u32));
__uint(value_size, sizeof(u32));
} events SEC(".maps");
static u64 (*bpf_get_current_pid_tgid)(void) = (void *)14;
static u64 (*bpf_get_current_uid_gid)(void) = (void *)15;
static long (*bpf_get_current_comm)(void *buf, u32 size) = (void *)16;
static long (*bpf_probe_read_user)(void *dst, u32 size, const void *unsafe_ptr) = (void *)112;
static long (*bpf_probe_read_user_str)(void *dst, u32 size, const void *unsafe_ptr) = (void *)114;
static long (*bpf_probe_read_kernel)(void *dst, u32 size, const void *unsafe_ptr) = (void *)113;
static long (*bpf_perf_event_output)(void *ctx, void *map, u64 flags,
void *data, u64 size) = (void *)25;
static u64 (*bpf_get_current_task)(void) = (void *)35;
#define bpf_core_read(dst, size, src) \
bpf_probe_read_kernel((dst), (size), \
(const void *)__builtin_preserve_access_index(src))
SEC("tracepoint/syscalls/sys_enter_execve")
int trace_execve(struct trace_event_raw_sys_enter *ctx)
{
struct exec_event event = {};
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
struct task_struct *parent = 0;
const char *const *argv = (const char *const *)ctx->args[1];
const char *argument = 0;
event.pid = bpf_get_current_pid_tgid() >> 32;
event.uid = bpf_get_current_uid_gid();
bpf_core_read(&parent, sizeof(parent), &task->real_parent);
if (parent)
bpf_core_read(&event.ppid, sizeof(event.ppid), &parent->tgid);
bpf_get_current_comm(event.parent_comm, sizeof(event.parent_comm));
bpf_probe_read_user_str(event.filename, sizeof(event.filename),
(const void *)ctx->args[0]);
bpf_probe_read_user(&argument, sizeof(argument), &argv[1]);
if (argument)
bpf_probe_read_user_str(event.arg1, sizeof(event.arg1), argument);
argument = 0;
bpf_probe_read_user(&argument, sizeof(argument), &argv[2]);
if (argument)
bpf_probe_read_user_str(event.arg2, sizeof(event.arg2), argument);
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &event, sizeof(event));
return 0;
}
char LICENSE[] SEC("license") = "GPL";

View file

@ -0,0 +1,212 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#define SEC(name) __attribute__((section(name), used))
#define __uint(name, value) int (*name)[value]
typedef unsigned int u32;
typedef unsigned short u16;
typedef unsigned long long u64;
#define BPF_MAP_TYPE_HASH 1
#define BPF_MAP_TYPE_PERF_EVENT_ARRAY 4
#define BPF_F_CURRENT_CPU 0xffffffffULL
struct trace_event_raw_sys_enter {
u64 _common;
long id;
unsigned long args[6];
};
struct trace_event_raw_sys_exit {
u64 _common;
long id;
long ret;
};
struct task_struct {
int tgid;
struct task_struct *real_parent;
} __attribute__((preserve_access_index));
struct syscall_event {
u32 pid;
u32 ppid;
u32 uid;
u32 syscall_id;
char comm[16];
u64 args[6];
char text[80];
};
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(key_size, sizeof(u64));
__uint(value_size, sizeof(u32));
__uint(max_entries, 64);
} monitored_syscalls SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(key_size, 16);
__uint(value_size, sizeof(u32));
__uint(max_entries, 64);
} monitored_host_comms SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(key_size, sizeof(u64));
__uint(value_size, sizeof(struct syscall_event));
__uint(max_entries, 4096);
} pending_returns SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(u32));
__uint(value_size, sizeof(u32));
} syscall_events SEC(".maps");
static u64 (*bpf_get_current_pid_tgid)(void) = (void *)14;
static u64 (*bpf_get_current_uid_gid)(void) = (void *)15;
static long (*bpf_get_current_comm)(void *buf, u32 size) = (void *)16;
static void *(*bpf_map_lookup_elem)(void *map, const void *key) = (void *)1;
static long (*bpf_map_update_elem)(void *map, const void *key, const void *value, u64 flags) = (void *)2;
static long (*bpf_map_delete_elem)(void *map, const void *key) = (void *)3;
static long (*bpf_probe_read_user_str)(void *dst, u32 size, const void *unsafe_ptr) = (void *)114;
static long (*bpf_probe_read_user)(void *dst, u32 size, const void *unsafe_ptr) = (void *)112;
static long (*bpf_probe_read_kernel)(void *dst, u32 size, const void *unsafe_ptr) = (void *)113;
static long (*bpf_perf_event_output)(void *ctx, void *map, u64 flags,
void *data, u64 size) = (void *)25;
static u64 (*bpf_get_current_task)(void) = (void *)35;
#define bpf_core_read(dst, size, src) \
bpf_probe_read_kernel((dst), (size), \
(const void *)__builtin_preserve_access_index(src))
SEC("tracepoint/raw_syscalls/sys_enter")
int trace_sys_enter(struct trace_event_raw_sys_enter *ctx)
{
u64 syscall_number = ctx->id;
u32 *syscall_id = bpf_map_lookup_elem(&monitored_syscalls, &syscall_number);
struct syscall_event event = {};
struct task_struct *task;
struct task_struct *parent = 0;
if (!syscall_id)
return 0;
task = (struct task_struct *)bpf_get_current_task();
event.pid = bpf_get_current_pid_tgid() >> 32;
event.uid = bpf_get_current_uid_gid();
event.syscall_id = *syscall_id;
bpf_core_read(&parent, sizeof(parent), &task->real_parent);
if (parent)
bpf_core_read(&event.ppid, sizeof(event.ppid), &parent->tgid);
bpf_get_current_comm(event.comm, sizeof(event.comm));
// Canonical ID 21 covers write/writev/pwrite variants. Keep the hot path
// in-kernel and emit only for process names that can satisfy the
// persistence-write host rule.
if (*syscall_id >= 21 && *syscall_id <= 26 &&
!bpf_map_lookup_elem(&monitored_host_comms, event.comm))
return 0;
// Match the Python BCC transport's semantic argument layout rather than
// exposing pointer-only raw ABI slots in alert keys and details.
if (*syscall_id == 3) { // memfd_create(name, flags)
event.args[0] = ctx->args[1];
bpf_probe_read_user_str(event.text, sizeof(event.text),
(const void *)ctx->args[0]);
} else if (*syscall_id == 7 || *syscall_id == 8) { // process_vm_{readv,writev}
event.args[0] = ctx->args[0]; // target pid
event.args[1] = ctx->args[2]; // local iovec count
event.args[2] = ctx->args[4]; // remote iovec count
event.args[3] = ctx->args[5]; // flags
} else if (*syscall_id == 14) { // chmod(path, mode)
event.args[0] = ctx->args[1];
event.args[2] = (u64)-100; // AT_FDCWD
bpf_probe_read_user_str(event.text, sizeof(event.text),
(const void *)ctx->args[0]);
} else if (*syscall_id == 15) { // fchmodat(dirfd, path, mode)
event.args[0] = ctx->args[2];
event.args[2] = ctx->args[0];
bpf_probe_read_user_str(event.text, sizeof(event.text),
(const void *)ctx->args[1]);
} else if (*syscall_id == 16 || *syscall_id == 17) { // chown/lchown(path, uid, gid)
event.args[0] = ctx->args[1];
event.args[1] = ctx->args[2];
event.args[2] = (u64)-100; // AT_FDCWD
bpf_probe_read_user_str(event.text, sizeof(event.text),
(const void *)ctx->args[0]);
} else if (*syscall_id == 18) { // fchownat(dirfd, path, uid, gid)
event.args[0] = ctx->args[2];
event.args[1] = ctx->args[3];
event.args[2] = ctx->args[0];
bpf_probe_read_user_str(event.text, sizeof(event.text),
(const void *)ctx->args[1]);
} else if (*syscall_id == 19) { // capset(header, data[2])
u32 effective_low = 0;
u32 effective_high = 0;
const char *data = (const char *)ctx->args[1];
bpf_probe_read_user(&effective_low, sizeof(effective_low), data);
bpf_probe_read_user(&effective_high, sizeof(effective_high), data + 12);
event.args[0] = effective_low | ((u64)effective_high << 32);
} else if (*syscall_id == 22 || *syscall_id == 23) { // connect/bind(fd, sockaddr, len)
const char *address = (const char *)ctx->args[1];
u16 family = 0;
u16 port = 0;
u32 ipv4 = 0;
bpf_probe_read_user(&family, sizeof(family), address);
bpf_probe_read_user(&port, sizeof(port), address + 2);
event.args[0] = family;
event.args[3] = ctx->args[0];
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
event.args[1] = __builtin_bswap16(port);
#else
event.args[1] = port;
#endif
if (family == 2) {
bpf_probe_read_user(&ipv4, sizeof(ipv4), address + 4);
event.args[2] = ipv4;
} else if (family == 10) {
bpf_probe_read_user(event.text, 16, address + 8);
}
} else if (*syscall_id >= 24 && *syscall_id <= 26) { // listen/accept/accept4
event.args[3] = ctx->args[0];
} else {
#pragma unroll
for (int i = 0; i < 6; i++)
event.args[i] = ctx->args[i];
}
if (*syscall_id >= 21 && *syscall_id <= 26) {
u64 pid_tgid = bpf_get_current_pid_tgid();
bpf_map_update_elem(&pending_returns, &pid_tgid, &event, 0);
return 0;
}
bpf_perf_event_output(ctx, &syscall_events, BPF_F_CURRENT_CPU, &event, sizeof(event));
return 0;
}
SEC("tracepoint/raw_syscalls/sys_exit")
int trace_sys_exit(struct trace_event_raw_sys_exit *ctx)
{
u64 pid_tgid = bpf_get_current_pid_tgid();
struct syscall_event *event = bpf_map_lookup_elem(&pending_returns, &pid_tgid);
if (!event)
return 0;
if (event->syscall_id == 25 || event->syscall_id == 26)
event->args[3] = ctx->ret;
// Every write-family syscall returns its byte count, so the shared
// canonical event is confirmed only when at least one byte was written.
if ((event->syscall_id == 21 && ctx->ret > 0) ||
(event->syscall_id >= 22 && event->syscall_id <= 24 && ctx->ret == 0) ||
((event->syscall_id == 25 || event->syscall_id == 26) && ctx->ret >= 0))
bpf_perf_event_output(ctx, &syscall_events, BPF_F_CURRENT_CPU,
event, sizeof(*event));
bpf_map_delete_elem(&pending_returns, &pid_tgid);
return 0;
}
char LICENSE[] SEC("license") = "GPL";

View file

@ -0,0 +1,131 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package ebpfsource
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/perf"
"github.com/cilium/ebpf/rlimit"
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/events"
)
// ExecSource owns the Go-native exec tracepoint and perf reader. Load errors
// are returned to the caller so startup can log and fall back to polling.
type ExecSource struct {
objects execObjects
link link.Link
reader *perf.Reader
}
// execEventRaw mirrors struct exec_event in bpf/exec.bpf.c. Keeping the wire
// layout explicit makes changes to the kernel/user-space ABI reviewable.
type execEventRaw struct {
PID uint32
PPID uint32
UID uint32
ParentComm [16]int8
Filename [128]int8
Arg1 [128]int8
Arg2 [128]int8
}
// LostSamplesError reports perf-buffer pressure without making it impossible
// for the caller to continue reading later events.
type LostSamplesError struct {
Count uint64
}
func (e LostSamplesError) Error() string {
return fmt.Sprintf("exec perf buffer lost %d samples", e.Count)
}
func OpenExecSource() (*ExecSource, error) {
// Required by older kernels. On newer memcg-accounted kernels this may fail
// in an otherwise capable service sandbox, so attempt the load regardless
// and preserve both errors only when loading also fails.
memlockErr := rlimit.RemoveMemlock()
var objects execObjects
if err := loadExecObjects(&objects, nil); err != nil {
if memlockErr != nil {
return nil, fmt.Errorf("load exec BPF objects: %w (memlock adjustment: %v)", err, memlockErr)
}
return nil, fmt.Errorf("load exec BPF objects: %w", err)
}
attached, err := link.Tracepoint("syscalls", "sys_enter_execve", objects.TraceExecve, nil)
if err != nil {
objects.Close()
return nil, fmt.Errorf("attach exec tracepoint: %w", err)
}
reader, err := perf.NewReader(objects.Events, 64*4096)
if err != nil {
attached.Close()
objects.Close()
return nil, fmt.Errorf("open exec perf reader: %w", err)
}
return &ExecSource{objects: objects, link: attached, reader: reader}, nil
}
func (s *ExecSource) Read() (events.ExecEvent, error) {
record, err := s.reader.Read()
if err != nil {
return events.ExecEvent{}, err
}
if record.LostSamples != 0 {
return events.ExecEvent{}, LostSamplesError{Count: record.LostSamples}
}
return decodeExecSample(record.RawSample)
}
func decodeExecSample(sample []byte) (events.ExecEvent, error) {
var raw execEventRaw
if len(sample) != binary.Size(raw) {
return events.ExecEvent{}, fmt.Errorf("exec event size %d, want %d", len(sample), binary.Size(raw))
}
if err := binary.Read(bytes.NewReader(sample), binary.NativeEndian, &raw); err != nil {
return events.ExecEvent{}, fmt.Errorf("decode exec event: %w", err)
}
argv := make([]string, 0, 2)
if value := cString(raw.Arg1[:]); value != "" {
argv = append(argv, value)
}
if value := cString(raw.Arg2[:]); value != "" {
argv = append(argv, value)
}
return events.ExecEvent{
PID: int(raw.PID), PPID: int(raw.PPID), UID: int(raw.UID),
ParentComm: cString(raw.ParentComm[:]), Filename: cString(raw.Filename[:]),
Argv: argv,
}, nil
}
func (s *ExecSource) Close() error {
var failures []error
if s.reader != nil {
failures = append(failures, s.reader.Close())
}
if s.link != nil {
failures = append(failures, s.link.Close())
}
failures = append(failures, s.objects.Close())
return errors.Join(failures...)
}
func cString(raw []int8) string {
value := make([]byte, 0, len(raw))
for _, char := range raw {
if char == 0 {
break
}
value = append(value, byte(char))
}
return string(value)
}
var _ io.Closer = (*ExecSource)(nil)

View file

@ -0,0 +1,141 @@
// Code generated by bpf2go; DO NOT EDIT.
//go:build mips || mips64 || ppc64 || s390x
package ebpfsource
import (
"bytes"
_ "embed"
"fmt"
"io"
"github.com/cilium/ebpf"
)
// Names of all BPF objects in the ELF.
//
// Used for safe lookups in a Collection or CollectionSpec.
const (
execMapEvents = "events"
execProgTraceExecve = "trace_execve"
)
// loadExec returns the embedded CollectionSpec for exec.
func loadExec() (*ebpf.CollectionSpec, error) {
reader := bytes.NewReader(_ExecBytes)
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
if err != nil {
return nil, fmt.Errorf("can't load exec: %w", err)
}
return spec, err
}
// loadExecObjects loads exec and converts it into a struct.
//
// The following types are suitable as obj argument:
//
// *execObjects
// *execPrograms
// *execMaps
//
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
func loadExecObjects(obj any, opts *ebpf.CollectionOptions) error {
spec, err := loadExec()
if err != nil {
return err
}
return spec.LoadAndAssign(obj, opts)
}
// execSpecs contains maps and programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type execSpecs struct {
execProgramSpecs
execMapSpecs
execVariableSpecs
}
// execProgramSpecs contains programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type execProgramSpecs struct {
TraceExecve *ebpf.ProgramSpec `ebpf:"trace_execve"`
}
// execMapSpecs contains maps before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type execMapSpecs struct {
Events *ebpf.MapSpec `ebpf:"events"`
}
// execVariableSpecs contains global variables before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type execVariableSpecs struct {
}
// execObjects contains all objects after they have been loaded into the kernel.
//
// It can be passed to loadExecObjects or ebpf.CollectionSpec.LoadAndAssign.
type execObjects struct {
execPrograms
execMaps
execVariables
}
func (o *execObjects) Close() error {
return _ExecClose(
&o.execPrograms,
&o.execMaps,
)
}
// execMaps contains all maps after they have been loaded into the kernel.
//
// It can be passed to loadExecObjects or ebpf.CollectionSpec.LoadAndAssign.
type execMaps struct {
Events *ebpf.Map `ebpf:"events"`
}
func (m *execMaps) Close() error {
return _ExecClose(
m.Events,
)
}
// execVariables contains all global variables after they have been loaded into the kernel.
//
// It can be passed to loadExecObjects or ebpf.CollectionSpec.LoadAndAssign.
type execVariables struct {
}
// execPrograms contains all programs after they have been loaded into the kernel.
//
// It can be passed to loadExecObjects or ebpf.CollectionSpec.LoadAndAssign.
type execPrograms struct {
TraceExecve *ebpf.Program `ebpf:"trace_execve"`
}
func (p *execPrograms) Close() error {
return _ExecClose(
p.TraceExecve,
)
}
func _ExecClose(closers ...io.Closer) error {
for _, closer := range closers {
if err := closer.Close(); err != nil {
return err
}
}
return nil
}
// Do not access this directly.
//
//go:embed exec_bpfeb.o
var _ExecBytes []byte

Binary file not shown.

View file

@ -0,0 +1,141 @@
// Code generated by bpf2go; DO NOT EDIT.
//go:build 386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 || wasm
package ebpfsource
import (
"bytes"
_ "embed"
"fmt"
"io"
"github.com/cilium/ebpf"
)
// Names of all BPF objects in the ELF.
//
// Used for safe lookups in a Collection or CollectionSpec.
const (
execMapEvents = "events"
execProgTraceExecve = "trace_execve"
)
// loadExec returns the embedded CollectionSpec for exec.
func loadExec() (*ebpf.CollectionSpec, error) {
reader := bytes.NewReader(_ExecBytes)
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
if err != nil {
return nil, fmt.Errorf("can't load exec: %w", err)
}
return spec, err
}
// loadExecObjects loads exec and converts it into a struct.
//
// The following types are suitable as obj argument:
//
// *execObjects
// *execPrograms
// *execMaps
//
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
func loadExecObjects(obj any, opts *ebpf.CollectionOptions) error {
spec, err := loadExec()
if err != nil {
return err
}
return spec.LoadAndAssign(obj, opts)
}
// execSpecs contains maps and programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type execSpecs struct {
execProgramSpecs
execMapSpecs
execVariableSpecs
}
// execProgramSpecs contains programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type execProgramSpecs struct {
TraceExecve *ebpf.ProgramSpec `ebpf:"trace_execve"`
}
// execMapSpecs contains maps before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type execMapSpecs struct {
Events *ebpf.MapSpec `ebpf:"events"`
}
// execVariableSpecs contains global variables before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type execVariableSpecs struct {
}
// execObjects contains all objects after they have been loaded into the kernel.
//
// It can be passed to loadExecObjects or ebpf.CollectionSpec.LoadAndAssign.
type execObjects struct {
execPrograms
execMaps
execVariables
}
func (o *execObjects) Close() error {
return _ExecClose(
&o.execPrograms,
&o.execMaps,
)
}
// execMaps contains all maps after they have been loaded into the kernel.
//
// It can be passed to loadExecObjects or ebpf.CollectionSpec.LoadAndAssign.
type execMaps struct {
Events *ebpf.Map `ebpf:"events"`
}
func (m *execMaps) Close() error {
return _ExecClose(
m.Events,
)
}
// execVariables contains all global variables after they have been loaded into the kernel.
//
// It can be passed to loadExecObjects or ebpf.CollectionSpec.LoadAndAssign.
type execVariables struct {
}
// execPrograms contains all programs after they have been loaded into the kernel.
//
// It can be passed to loadExecObjects or ebpf.CollectionSpec.LoadAndAssign.
type execPrograms struct {
TraceExecve *ebpf.Program `ebpf:"trace_execve"`
}
func (p *execPrograms) Close() error {
return _ExecClose(
p.TraceExecve,
)
}
func _ExecClose(closers ...io.Closer) error {
for _, closer := range closers {
if err := closer.Close(); err != nil {
return err
}
}
return nil
}
// Do not access this directly.
//
//go:embed exec_bpfel.o
var _ExecBytes []byte

Binary file not shown.

View file

@ -0,0 +1,52 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package ebpfsource
import (
"bytes"
"encoding/binary"
"testing"
)
func TestDecodeExecSample(t *testing.T) {
raw := execEventRaw{PID: 42, PPID: 7, UID: 1000}
copyCString(raw.ParentComm[:], "nginx")
copyCString(raw.Filename[:], "/bin/bash")
copyCString(raw.Arg1[:], "-c")
copyCString(raw.Arg2[:], "id")
var sample bytes.Buffer
if err := binary.Write(&sample, binary.NativeEndian, raw); err != nil {
t.Fatal(err)
}
event, err := decodeExecSample(sample.Bytes())
if err != nil {
t.Fatal(err)
}
if event.PID != 42 || event.PPID != 7 || event.UID != 1000 ||
event.ParentComm != "nginx" || event.Filename != "/bin/bash" ||
len(event.Argv) != 2 || event.Argv[0] != "-c" || event.Argv[1] != "id" {
t.Fatalf("unexpected event: %#v", event)
}
}
func TestDecodeExecSampleRejectsWrongSize(t *testing.T) {
if _, err := decodeExecSample([]byte{1, 2, 3}); err == nil {
t.Fatal("expected size error")
}
}
func TestCStringStopsAtNUL(t *testing.T) {
if got := cString([]int8{'a', 'b', 0, 'c'}); got != "ab" {
t.Fatalf("got %q", got)
}
}
func copyCString(destination []int8, value string) {
for index, char := range []byte(value) {
if index >= len(destination)-1 {
break
}
destination[index] = int8(char)
}
}

View file

@ -0,0 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package ebpfsource
// Generate checked-in object bindings so normal builds need neither clang nor
// kernel headers. The tracepoint ABI is stable and avoids host-specific
// generated vmlinux.h while the loader remains pure Go.
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go@v0.22.0 -cc clang -cflags "-O2 -g -Wall -Werror" exec bpf/exec.bpf.c
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go@v0.22.0 -cc clang -cflags "-O2 -g -Wall -Werror" syscall bpf/syscall.bpf.c

View file

@ -0,0 +1,75 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package ebpfsource
import (
"testing"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/btf"
)
func TestEmbeddedExecCollectionShape(t *testing.T) {
specification, err := loadExec()
if err != nil {
t.Fatal(err)
}
program := specification.Programs["trace_execve"]
eventsMap := specification.Maps["events"]
if program == nil || program.Type != ebpf.TracePoint || program.SectionName != "tracepoint/syscalls/sys_enter_execve" {
t.Fatalf("unexpected exec program: %#v", program)
}
if eventsMap == nil || eventsMap.Type != ebpf.PerfEventArray {
t.Fatalf("unexpected exec events map: %#v", eventsMap)
}
if countCORERelocations(program) < 2 {
t.Fatal("exec program must retain parent task CO-RE relocations")
}
}
func TestEmbeddedSyscallCollectionShape(t *testing.T) {
specification, err := loadSyscall()
if err != nil {
t.Fatal(err)
}
program := specification.Programs["trace_sys_enter"]
exitProgram := specification.Programs["trace_sys_exit"]
filterMap := specification.Maps["monitored_syscalls"]
writeCommsMap := specification.Maps["monitored_host_comms"]
pendingWritesMap := specification.Maps["pending_returns"]
eventsMap := specification.Maps["syscall_events"]
if program == nil || program.Type != ebpf.TracePoint || program.SectionName != "tracepoint/raw_syscalls/sys_enter" {
t.Fatalf("unexpected syscall program: %#v", program)
}
if exitProgram == nil || exitProgram.Type != ebpf.TracePoint || exitProgram.SectionName != "tracepoint/raw_syscalls/sys_exit" {
t.Fatalf("unexpected syscall exit program: %#v", exitProgram)
}
if filterMap == nil || filterMap.Type != ebpf.Hash || filterMap.MaxEntries != 64 ||
filterMap.KeySize != 8 || filterMap.ValueSize != 4 {
t.Fatalf("unexpected syscall filter map: %#v", filterMap)
}
if writeCommsMap == nil || writeCommsMap.Type != ebpf.Hash || writeCommsMap.MaxEntries != 64 ||
writeCommsMap.KeySize != 16 || writeCommsMap.ValueSize != 4 {
t.Fatalf("unexpected write comm map: %#v", writeCommsMap)
}
if pendingWritesMap == nil || pendingWritesMap.Type != ebpf.Hash || pendingWritesMap.MaxEntries != 4096 ||
pendingWritesMap.KeySize != 8 {
t.Fatalf("unexpected pending writes map: %#v", pendingWritesMap)
}
if eventsMap == nil || eventsMap.Type != ebpf.PerfEventArray {
t.Fatalf("unexpected syscall events map: %#v", eventsMap)
}
if countCORERelocations(program) < 2 {
t.Fatal("syscall program must retain parent task CO-RE relocations")
}
}
func countCORERelocations(program *ebpf.ProgramSpec) int {
count := 0
for index := range program.Instructions {
if btf.CORERelocationMetadata(&program.Instructions[index]) != nil {
count++
}
}
return count
}

View file

@ -0,0 +1,476 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package ebpfsource
import (
"bufio"
"bytes"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"io"
"net"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/perf"
"github.com/cilium/ebpf/rlimit"
"golang.org/x/sys/unix"
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/events"
)
const (
syscallMprotect uint32 = iota + 1
syscallMmap
syscallMemfdCreate
syscallPtrace
syscallPrctl
syscallSeccomp
syscallProcessVMReadv
syscallProcessVMWritev
syscallMlock
syscallMlock2
syscallMlockall
hostSetuid
hostSetgid
hostChmod
hostFchmodat
hostChown
hostLchown
hostFchownat
hostCapset
hostFinitModule
hostFileWrite
hostTCPConnect
hostBind
hostListen
hostAccept
hostAccept4
)
var syscallNames = map[uint32]string{
syscallMprotect: "mprotect",
syscallMmap: "mmap",
syscallMemfdCreate: "memfd_create",
syscallPtrace: "ptrace",
syscallPrctl: "prctl",
syscallSeccomp: "seccomp",
syscallProcessVMReadv: "process_vm_readv",
syscallProcessVMWritev: "process_vm_writev",
syscallMlock: "mlock",
syscallMlock2: "mlock2",
syscallMlockall: "mlockall",
}
type syscallEventRaw struct {
PID uint32
PPID uint32
UID uint32
SyscallID uint32
Comm [16]int8
Args [6]uint64
Text [80]int8
}
type SyscallLostSamplesError struct {
Count uint64
}
// SecurityEvent is the typed result of the shared filtered syscall transport.
// Exactly one field is populated.
type SecurityEvent struct {
Syscall *events.SyscallEvent
Host *events.HostEvent
pathDirFD int64
}
func (e SyscallLostSamplesError) Error() string {
return fmt.Sprintf("syscall perf buffer lost %d samples", e.Count)
}
// SyscallSource filters raw syscall entry events in-kernel using a map of
// architecture-specific syscall numbers, then exposes transport-neutral
// SyscallEvent values to the existing rule engine.
type SyscallSource struct {
objects syscallObjects
links []link.Link
reader *perf.Reader
procRoot string
}
func OpenSyscallSource(procRoot string) (*SyscallSource, error) {
if procRoot == "" {
procRoot = "/proc"
}
numbers := monitoredSyscalls()
memlockErr := rlimit.RemoveMemlock()
var objects syscallObjects
if err := loadSyscallObjects(&objects, nil); err != nil {
if memlockErr != nil {
return nil, fmt.Errorf("load syscall BPF objects: %w (memlock adjustment: %v)", err, memlockErr)
}
return nil, fmt.Errorf("load syscall BPF objects: %w", err)
}
for number, identifier := range numbers {
if err := objects.MonitoredSyscalls.Update(number, identifier, ebpf.UpdateAny); err != nil {
objects.Close()
return nil, fmt.Errorf("configure syscall %d: %w", number, err)
}
}
for _, comm := range events.HostInterpreterComms() {
var key [16]byte
copy(key[:], comm)
if err := objects.MonitoredHostComms.Update(key, uint32(1), ebpf.UpdateAny); err != nil {
objects.Close()
return nil, fmt.Errorf("configure write comm %q: %w", comm, err)
}
}
enterLink, err := link.Tracepoint("raw_syscalls", "sys_enter", objects.TraceSysEnter, nil)
if err != nil {
objects.Close()
return nil, fmt.Errorf("attach raw syscall tracepoint: %w", err)
}
exitLink, err := link.Tracepoint("raw_syscalls", "sys_exit", objects.TraceSysExit, nil)
if err != nil {
enterLink.Close()
objects.Close()
return nil, fmt.Errorf("attach raw syscall exit tracepoint: %w", err)
}
reader, err := perf.NewReader(objects.SyscallEvents, 64*4096)
if err != nil {
exitLink.Close()
enterLink.Close()
objects.Close()
return nil, fmt.Errorf("open syscall perf reader: %w", err)
}
return &SyscallSource{objects: objects, links: []link.Link{enterLink, exitLink}, reader: reader, procRoot: procRoot}, nil
}
func (s *SyscallSource) Read() (SecurityEvent, error) {
record, err := s.reader.Read()
if err != nil {
return SecurityEvent{}, err
}
if record.LostSamples != 0 {
return SecurityEvent{}, SyscallLostSamplesError{Count: record.LostSamples}
}
securityEvent, err := decodeSecuritySample(record.RawSample)
if err == nil && securityEvent.Host != nil {
if securityEvent.Host.Path != "" {
securityEvent.Host.Path = resolveProcessPath(
s.procRoot, securityEvent.Host.PID, securityEvent.pathDirFD,
securityEvent.Host.Path,
)
} else if securityEvent.Host.Event == "module_load" || securityEvent.Host.Event == "file_write" {
securityEvent.Host.Path = resolveProcessFD(s.procRoot, securityEvent.Host.PID, securityEvent.pathDirFD)
if securityEvent.Host.Event == "module_load" && securityEvent.Host.Path != "" {
securityEvent.Host.ModuleName = filepath.Base(strings.TrimSuffix(securityEvent.Host.Path, " (deleted)"))
}
}
if securityEvent.Host.Event == "tcp_connect" &&
!isTCPSocket(s.procRoot, securityEvent.Host.PID, securityEvent.pathDirFD) {
securityEvent.Host = nil
}
if securityEvent.Host != nil &&
(securityEvent.Host.Event == "listen" || securityEvent.Host.Event == "accept") {
local, peer, ok := lookupTCPSocket(s.procRoot, securityEvent.Host.PID, securityEvent.pathDirFD)
if !ok {
securityEvent.Host = nil
} else {
securityEvent.Host.LocalIP, securityEvent.Host.LocalPort = local.IP, local.Port
securityEvent.Host.PeerIP, securityEvent.Host.PeerPort = peer.IP, peer.Port
}
}
}
return securityEvent, err
}
func decodeSecuritySample(sample []byte) (SecurityEvent, error) {
var raw syscallEventRaw
if len(sample) != binary.Size(raw) {
return SecurityEvent{}, fmt.Errorf("syscall event size %d, want %d", len(sample), binary.Size(raw))
}
if err := binary.Read(bytes.NewReader(sample), binary.NativeEndian, &raw); err != nil {
return SecurityEvent{}, fmt.Errorf("decode syscall event: %w", err)
}
if raw.SyscallID == hostSetuid || raw.SyscallID == hostSetgid {
event := events.HostEvent{
PID: int(raw.PID), PPID: int(raw.PPID), UID: int(raw.UID),
Comm: cString(raw.Comm[:]), TargetUID: -1, TargetGID: -1,
}
if raw.SyscallID == hostSetuid {
event.Event = "setuid"
event.TargetUID = int(raw.Args[0])
} else {
event.Event = "setgid"
event.TargetGID = int(raw.Args[0])
}
return SecurityEvent{Host: &event}, nil
}
if raw.SyscallID >= hostChmod && raw.SyscallID <= hostFchownat {
event := events.HostEvent{
PID: int(raw.PID), PPID: int(raw.PPID), UID: int(raw.UID),
Comm: cString(raw.Comm[:]), Path: cString(raw.Text[:]),
TargetUID: -1, TargetGID: -1,
}
switch raw.SyscallID {
case hostChmod, hostFchmodat:
event.Event = "chmod"
event.Mode = int(raw.Args[0])
case hostChown, hostLchown, hostFchownat:
event.Event = "chown"
event.TargetUID = int(raw.Args[0])
event.TargetGID = int(raw.Args[1])
}
return SecurityEvent{Host: &event, pathDirFD: int64(raw.Args[2])}, nil
}
if raw.SyscallID == hostCapset {
event := events.HostEvent{
Event: "capset", PID: int(raw.PID), PPID: int(raw.PPID), UID: int(raw.UID),
Comm: cString(raw.Comm[:]), TargetUID: -1, TargetGID: -1,
Capabilities: capabilityNames(raw.Args[0]),
}
return SecurityEvent{Host: &event}, nil
}
if raw.SyscallID == hostFinitModule {
event := events.HostEvent{
Event: "module_load", PID: int(raw.PID), PPID: int(raw.PPID), UID: int(raw.UID),
Comm: cString(raw.Comm[:]), TargetUID: -1, TargetGID: -1,
}
return SecurityEvent{Host: &event, pathDirFD: int64(raw.Args[0])}, nil
}
if raw.SyscallID == hostFileWrite {
event := events.HostEvent{
Event: "file_write", PID: int(raw.PID), PPID: int(raw.PPID), UID: int(raw.UID),
Comm: cString(raw.Comm[:]), TargetUID: -1, TargetGID: -1,
}
return SecurityEvent{Host: &event, pathDirFD: int64(raw.Args[0])}, nil
}
if raw.SyscallID == hostTCPConnect || raw.SyscallID == hostBind {
event := events.HostEvent{
PID: int(raw.PID), PPID: int(raw.PPID), UID: int(raw.UID),
Comm: cString(raw.Comm[:]), TargetUID: -1, TargetGID: -1,
}
address := socketAddress(raw)
if raw.SyscallID == hostTCPConnect {
event.Event, event.PeerIP, event.PeerPort = "tcp_connect", address, int(raw.Args[1])
} else {
event.Event, event.LocalIP, event.LocalPort = "bind", address, int(raw.Args[1])
}
return SecurityEvent{Host: &event, pathDirFD: int64(raw.Args[3])}, nil
}
if raw.SyscallID >= hostListen && raw.SyscallID <= hostAccept4 {
eventName := "accept"
if raw.SyscallID == hostListen {
eventName = "listen"
}
event := events.HostEvent{
Event: eventName, PID: int(raw.PID), PPID: int(raw.PPID), UID: int(raw.UID),
Comm: cString(raw.Comm[:]), TargetUID: -1, TargetGID: -1,
}
return SecurityEvent{Host: &event, pathDirFD: int64(raw.Args[3])}, nil
}
name, ok := syscallNames[raw.SyscallID]
if !ok {
return SecurityEvent{}, fmt.Errorf("unknown canonical syscall id %d", raw.SyscallID)
}
event := events.SyscallEvent{
PID: int(raw.PID), PPID: int(raw.PPID), UID: int(raw.UID),
Comm: cString(raw.Comm[:]), Syscall: name, Args: raw.Args,
Text: cString(raw.Text[:]),
}
return SecurityEvent{Syscall: &event}, nil
}
func decodeSyscallSample(sample []byte) (events.SyscallEvent, error) {
securityEvent, err := decodeSecuritySample(sample)
if err != nil {
return events.SyscallEvent{}, err
}
if securityEvent.Syscall == nil {
return events.SyscallEvent{}, fmt.Errorf("sample is a typed host event")
}
return *securityEvent.Syscall, nil
}
func (s *SyscallSource) Close() error {
var failures []error
if s.reader != nil {
failures = append(failures, s.reader.Close())
}
for _, attached := range s.links {
if attached != nil {
failures = append(failures, attached.Close())
}
}
failures = append(failures, s.objects.Close())
return errors.Join(failures...)
}
func monitoredSyscalls() map[uint64]uint32 {
numbers := map[uint64]uint32{
unix.SYS_MPROTECT: syscallMprotect,
unix.SYS_MMAP: syscallMmap,
unix.SYS_MEMFD_CREATE: syscallMemfdCreate,
unix.SYS_PTRACE: syscallPtrace,
unix.SYS_PRCTL: syscallPrctl,
unix.SYS_SECCOMP: syscallSeccomp,
unix.SYS_PROCESS_VM_READV: syscallProcessVMReadv,
unix.SYS_PROCESS_VM_WRITEV: syscallProcessVMWritev,
unix.SYS_MLOCK: syscallMlock,
unix.SYS_MLOCK2: syscallMlock2,
unix.SYS_MLOCKALL: syscallMlockall,
unix.SYS_SETUID: hostSetuid,
unix.SYS_SETGID: hostSetgid,
unix.SYS_FCHMODAT: hostFchmodat,
unix.SYS_FCHOWNAT: hostFchownat,
unix.SYS_CAPSET: hostCapset,
unix.SYS_FINIT_MODULE: hostFinitModule,
unix.SYS_WRITE: hostFileWrite,
unix.SYS_WRITEV: hostFileWrite,
unix.SYS_PWRITE64: hostFileWrite,
unix.SYS_PWRITEV: hostFileWrite,
unix.SYS_PWRITEV2: hostFileWrite,
unix.SYS_CONNECT: hostTCPConnect,
unix.SYS_BIND: hostBind,
unix.SYS_LISTEN: hostListen,
unix.SYS_ACCEPT4: hostAccept4,
}
for number, identifier := range legacyPermissionSyscalls() {
numbers[number] = identifier
}
for number, identifier := range legacyNetworkSyscalls() {
numbers[number] = identifier
}
return numbers
}
func socketAddress(raw syscallEventRaw) string {
switch raw.Args[0] {
case unix.AF_INET:
bytes := make([]byte, 4)
binary.NativeEndian.PutUint32(bytes, uint32(raw.Args[2]))
return net.IP(bytes).String()
case unix.AF_INET6:
bytes := make([]byte, 16)
for index := range bytes {
bytes[index] = byte(raw.Text[index])
}
return net.IP(bytes).String()
default:
return ""
}
}
func capabilityNames(effective uint64) []string {
known := []struct {
bit uint
name string
}{
{0, "CAP_CHOWN"}, {1, "CAP_DAC_OVERRIDE"}, {2, "CAP_DAC_READ_SEARCH"},
{12, "CAP_NET_ADMIN"}, {13, "CAP_NET_RAW"}, {16, "CAP_SYS_MODULE"},
{19, "CAP_SYS_PTRACE"}, {21, "CAP_SYS_ADMIN"},
}
names := make([]string, 0)
for _, capability := range known {
if effective&(uint64(1)<<capability.bit) != 0 {
names = append(names, capability.name)
}
}
return names
}
func resolveProcessPath(procRoot string, pid int, dirFD int64, path string) string {
if filepath.IsAbs(path) {
return filepath.Clean(path)
}
processRoot := filepath.Join(procRoot, fmt.Sprintf("%d", pid))
baseLink := filepath.Join(processRoot, "cwd")
if dirFD >= 0 {
baseLink = filepath.Join(processRoot, "fd", fmt.Sprintf("%d", dirFD))
}
base, err := os.Readlink(baseLink)
if err != nil || !filepath.IsAbs(base) {
return path
}
return filepath.Clean(filepath.Join(base, path))
}
func resolveProcessFD(procRoot string, pid int, fd int64) string {
if fd < 0 {
return ""
}
path, err := os.Readlink(filepath.Join(procRoot, fmt.Sprintf("%d", pid), "fd", fmt.Sprintf("%d", fd)))
if err != nil || !filepath.IsAbs(path) {
return ""
}
return filepath.Clean(path)
}
func isTCPSocket(procRoot string, pid int, fd int64) bool {
_, _, ok := lookupTCPSocket(procRoot, pid, fd)
return ok
}
type socketEndpoint struct {
IP string
Port int
}
func lookupTCPSocket(procRoot string, pid int, fd int64) (socketEndpoint, socketEndpoint, bool) {
target, err := os.Readlink(filepath.Join(procRoot, fmt.Sprintf("%d", pid), "fd", fmt.Sprintf("%d", fd)))
if err != nil || !strings.HasPrefix(target, "socket:[") || !strings.HasSuffix(target, "]") {
return socketEndpoint{}, socketEndpoint{}, false
}
inode := strings.TrimSuffix(strings.TrimPrefix(target, "socket:["), "]")
for _, name := range []string{"tcp", "tcp6"} {
file, err := os.Open(filepath.Join(procRoot, "net", name))
if err != nil {
continue
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) > 9 && fields[9] == inode {
local, localErr := decodeProcEndpoint(fields[1], name == "tcp6")
peer, peerErr := decodeProcEndpoint(fields[2], name == "tcp6")
file.Close()
return local, peer, localErr == nil && peerErr == nil
}
}
file.Close()
}
return socketEndpoint{}, socketEndpoint{}, false
}
func decodeProcEndpoint(value string, ipv6 bool) (socketEndpoint, error) {
addressHex, portHex, ok := strings.Cut(value, ":")
if !ok {
return socketEndpoint{}, fmt.Errorf("invalid proc endpoint %q", value)
}
address, err := hex.DecodeString(addressHex)
if err != nil || len(address) != 4 && len(address) != 16 {
return socketEndpoint{}, fmt.Errorf("invalid proc address %q", addressHex)
}
if ipv6 {
for offset := 0; offset < len(address); offset += 4 {
slices.Reverse(address[offset : offset+4])
}
} else {
slices.Reverse(address)
}
port, err := strconv.ParseUint(portHex, 16, 16)
if err != nil {
return socketEndpoint{}, err
}
return socketEndpoint{IP: net.IP(address).String(), Port: int(port)}, nil
}
var _ io.Closer = (*SyscallSource)(nil)

View file

@ -0,0 +1,157 @@
// Code generated by bpf2go; DO NOT EDIT.
//go:build mips || mips64 || ppc64 || s390x
package ebpfsource
import (
"bytes"
_ "embed"
"fmt"
"io"
"github.com/cilium/ebpf"
)
// Names of all BPF objects in the ELF.
//
// Used for safe lookups in a Collection or CollectionSpec.
const (
syscallMapMonitoredHostComms = "monitored_host_comms"
syscallMapMonitoredSyscalls = "monitored_syscalls"
syscallMapPendingReturns = "pending_returns"
syscallMapSyscallEvents = "syscall_events"
syscallProgTraceSysEnter = "trace_sys_enter"
syscallProgTraceSysExit = "trace_sys_exit"
)
// loadSyscall returns the embedded CollectionSpec for syscall.
func loadSyscall() (*ebpf.CollectionSpec, error) {
reader := bytes.NewReader(_SyscallBytes)
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
if err != nil {
return nil, fmt.Errorf("can't load syscall: %w", err)
}
return spec, err
}
// loadSyscallObjects loads syscall and converts it into a struct.
//
// The following types are suitable as obj argument:
//
// *syscallObjects
// *syscallPrograms
// *syscallMaps
//
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
func loadSyscallObjects(obj any, opts *ebpf.CollectionOptions) error {
spec, err := loadSyscall()
if err != nil {
return err
}
return spec.LoadAndAssign(obj, opts)
}
// syscallSpecs contains maps and programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type syscallSpecs struct {
syscallProgramSpecs
syscallMapSpecs
syscallVariableSpecs
}
// syscallProgramSpecs contains programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type syscallProgramSpecs struct {
TraceSysEnter *ebpf.ProgramSpec `ebpf:"trace_sys_enter"`
TraceSysExit *ebpf.ProgramSpec `ebpf:"trace_sys_exit"`
}
// syscallMapSpecs contains maps before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type syscallMapSpecs struct {
MonitoredHostComms *ebpf.MapSpec `ebpf:"monitored_host_comms"`
MonitoredSyscalls *ebpf.MapSpec `ebpf:"monitored_syscalls"`
PendingReturns *ebpf.MapSpec `ebpf:"pending_returns"`
SyscallEvents *ebpf.MapSpec `ebpf:"syscall_events"`
}
// syscallVariableSpecs contains global variables before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type syscallVariableSpecs struct {
}
// syscallObjects contains all objects after they have been loaded into the kernel.
//
// It can be passed to loadSyscallObjects or ebpf.CollectionSpec.LoadAndAssign.
type syscallObjects struct {
syscallPrograms
syscallMaps
syscallVariables
}
func (o *syscallObjects) Close() error {
return _SyscallClose(
&o.syscallPrograms,
&o.syscallMaps,
)
}
// syscallMaps contains all maps after they have been loaded into the kernel.
//
// It can be passed to loadSyscallObjects or ebpf.CollectionSpec.LoadAndAssign.
type syscallMaps struct {
MonitoredHostComms *ebpf.Map `ebpf:"monitored_host_comms"`
MonitoredSyscalls *ebpf.Map `ebpf:"monitored_syscalls"`
PendingReturns *ebpf.Map `ebpf:"pending_returns"`
SyscallEvents *ebpf.Map `ebpf:"syscall_events"`
}
func (m *syscallMaps) Close() error {
return _SyscallClose(
m.MonitoredHostComms,
m.MonitoredSyscalls,
m.PendingReturns,
m.SyscallEvents,
)
}
// syscallVariables contains all global variables after they have been loaded into the kernel.
//
// It can be passed to loadSyscallObjects or ebpf.CollectionSpec.LoadAndAssign.
type syscallVariables struct {
}
// syscallPrograms contains all programs after they have been loaded into the kernel.
//
// It can be passed to loadSyscallObjects or ebpf.CollectionSpec.LoadAndAssign.
type syscallPrograms struct {
TraceSysEnter *ebpf.Program `ebpf:"trace_sys_enter"`
TraceSysExit *ebpf.Program `ebpf:"trace_sys_exit"`
}
func (p *syscallPrograms) Close() error {
return _SyscallClose(
p.TraceSysEnter,
p.TraceSysExit,
)
}
func _SyscallClose(closers ...io.Closer) error {
for _, closer := range closers {
if err := closer.Close(); err != nil {
return err
}
}
return nil
}
// Do not access this directly.
//
//go:embed syscall_bpfeb.o
var _SyscallBytes []byte

Binary file not shown.

View file

@ -0,0 +1,157 @@
// Code generated by bpf2go; DO NOT EDIT.
//go:build 386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 || wasm
package ebpfsource
import (
"bytes"
_ "embed"
"fmt"
"io"
"github.com/cilium/ebpf"
)
// Names of all BPF objects in the ELF.
//
// Used for safe lookups in a Collection or CollectionSpec.
const (
syscallMapMonitoredHostComms = "monitored_host_comms"
syscallMapMonitoredSyscalls = "monitored_syscalls"
syscallMapPendingReturns = "pending_returns"
syscallMapSyscallEvents = "syscall_events"
syscallProgTraceSysEnter = "trace_sys_enter"
syscallProgTraceSysExit = "trace_sys_exit"
)
// loadSyscall returns the embedded CollectionSpec for syscall.
func loadSyscall() (*ebpf.CollectionSpec, error) {
reader := bytes.NewReader(_SyscallBytes)
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
if err != nil {
return nil, fmt.Errorf("can't load syscall: %w", err)
}
return spec, err
}
// loadSyscallObjects loads syscall and converts it into a struct.
//
// The following types are suitable as obj argument:
//
// *syscallObjects
// *syscallPrograms
// *syscallMaps
//
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
func loadSyscallObjects(obj any, opts *ebpf.CollectionOptions) error {
spec, err := loadSyscall()
if err != nil {
return err
}
return spec.LoadAndAssign(obj, opts)
}
// syscallSpecs contains maps and programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type syscallSpecs struct {
syscallProgramSpecs
syscallMapSpecs
syscallVariableSpecs
}
// syscallProgramSpecs contains programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type syscallProgramSpecs struct {
TraceSysEnter *ebpf.ProgramSpec `ebpf:"trace_sys_enter"`
TraceSysExit *ebpf.ProgramSpec `ebpf:"trace_sys_exit"`
}
// syscallMapSpecs contains maps before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type syscallMapSpecs struct {
MonitoredHostComms *ebpf.MapSpec `ebpf:"monitored_host_comms"`
MonitoredSyscalls *ebpf.MapSpec `ebpf:"monitored_syscalls"`
PendingReturns *ebpf.MapSpec `ebpf:"pending_returns"`
SyscallEvents *ebpf.MapSpec `ebpf:"syscall_events"`
}
// syscallVariableSpecs contains global variables before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type syscallVariableSpecs struct {
}
// syscallObjects contains all objects after they have been loaded into the kernel.
//
// It can be passed to loadSyscallObjects or ebpf.CollectionSpec.LoadAndAssign.
type syscallObjects struct {
syscallPrograms
syscallMaps
syscallVariables
}
func (o *syscallObjects) Close() error {
return _SyscallClose(
&o.syscallPrograms,
&o.syscallMaps,
)
}
// syscallMaps contains all maps after they have been loaded into the kernel.
//
// It can be passed to loadSyscallObjects or ebpf.CollectionSpec.LoadAndAssign.
type syscallMaps struct {
MonitoredHostComms *ebpf.Map `ebpf:"monitored_host_comms"`
MonitoredSyscalls *ebpf.Map `ebpf:"monitored_syscalls"`
PendingReturns *ebpf.Map `ebpf:"pending_returns"`
SyscallEvents *ebpf.Map `ebpf:"syscall_events"`
}
func (m *syscallMaps) Close() error {
return _SyscallClose(
m.MonitoredHostComms,
m.MonitoredSyscalls,
m.PendingReturns,
m.SyscallEvents,
)
}
// syscallVariables contains all global variables after they have been loaded into the kernel.
//
// It can be passed to loadSyscallObjects or ebpf.CollectionSpec.LoadAndAssign.
type syscallVariables struct {
}
// syscallPrograms contains all programs after they have been loaded into the kernel.
//
// It can be passed to loadSyscallObjects or ebpf.CollectionSpec.LoadAndAssign.
type syscallPrograms struct {
TraceSysEnter *ebpf.Program `ebpf:"trace_sys_enter"`
TraceSysExit *ebpf.Program `ebpf:"trace_sys_exit"`
}
func (p *syscallPrograms) Close() error {
return _SyscallClose(
p.TraceSysEnter,
p.TraceSysExit,
)
}
func _SyscallClose(closers ...io.Closer) error {
for _, closer := range closers {
if err := closer.Close(); err != nil {
return err
}
}
return nil
}
// Do not access this directly.
//
//go:embed syscall_bpfel.o
var _SyscallBytes []byte

Binary file not shown.

View file

@ -0,0 +1,256 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package ebpfsource
import (
"bytes"
"encoding/binary"
"os"
"path/filepath"
"slices"
"testing"
"golang.org/x/sys/unix"
)
func TestDecodeSyscallSample(t *testing.T) {
raw := syscallEventRaw{
PID: 42, PPID: 7, UID: 1000, SyscallID: syscallMprotect,
Args: [6]uint64{0x1000, 0x2000, 0x6},
}
copyCString(raw.Comm[:], "dropper")
var sample bytes.Buffer
if err := binary.Write(&sample, binary.NativeEndian, raw); err != nil {
t.Fatal(err)
}
event, err := decodeSyscallSample(sample.Bytes())
if err != nil {
t.Fatal(err)
}
if event.PID != 42 || event.PPID != 7 || event.UID != 1000 ||
event.Comm != "dropper" || event.Syscall != "mprotect" || event.Args[2] != 0x6 {
t.Fatalf("unexpected event: %#v", event)
}
}
func TestDecodeSyscallSampleRejectsUnknownIDAndSize(t *testing.T) {
if _, err := decodeSyscallSample([]byte{1}); err == nil {
t.Fatal("expected size error")
}
raw := syscallEventRaw{SyscallID: 99}
var sample bytes.Buffer
if err := binary.Write(&sample, binary.NativeEndian, raw); err != nil {
t.Fatal(err)
}
if _, err := decodeSyscallSample(sample.Bytes()); err == nil {
t.Fatal("expected unknown ID error")
}
}
func TestDecodeSecuritySampleProducesTypedHostTransition(t *testing.T) {
raw := syscallEventRaw{
PID: 42, PPID: 7, UID: 1000, SyscallID: hostSetuid,
Args: [6]uint64{0},
}
copyCString(raw.Comm[:], "python3")
var sample bytes.Buffer
if err := binary.Write(&sample, binary.NativeEndian, raw); err != nil {
t.Fatal(err)
}
securityEvent, err := decodeSecuritySample(sample.Bytes())
if err != nil {
t.Fatal(err)
}
if securityEvent.Syscall != nil || securityEvent.Host == nil ||
securityEvent.Host.Event != "setuid" || securityEvent.Host.TargetUID != 0 ||
securityEvent.Host.TargetGID != -1 || securityEvent.Host.Comm != "python3" {
t.Fatalf("unexpected security event: %#v", securityEvent)
}
}
func TestDecodeSecuritySampleProducesPermissionChange(t *testing.T) {
raw := syscallEventRaw{
PID: 42, PPID: 7, UID: 1000, SyscallID: hostFchownat,
Args: [6]uint64{0, 0},
}
copyCString(raw.Comm[:], "python3")
copyCString(raw.Text[:], "/etc/systemd/system/backdoor.service")
var sample bytes.Buffer
if err := binary.Write(&sample, binary.NativeEndian, raw); err != nil {
t.Fatal(err)
}
securityEvent, err := decodeSecuritySample(sample.Bytes())
if err != nil {
t.Fatal(err)
}
if securityEvent.Host == nil || securityEvent.Host.Event != "chown" ||
securityEvent.Host.Path != "/etc/systemd/system/backdoor.service" ||
securityEvent.Host.TargetUID != 0 || securityEvent.Host.TargetGID != 0 {
t.Fatalf("unexpected security event: %#v", securityEvent)
}
}
func TestDecodeSecuritySampleProducesCapabilities(t *testing.T) {
raw := syscallEventRaw{
PID: 42, PPID: 7, UID: 1000, SyscallID: hostCapset,
Args: [6]uint64{(1 << 2) | (1 << 21)},
}
copyCString(raw.Comm[:], "python3")
var sample bytes.Buffer
if err := binary.Write(&sample, binary.NativeEndian, raw); err != nil {
t.Fatal(err)
}
securityEvent, err := decodeSecuritySample(sample.Bytes())
if err != nil {
t.Fatal(err)
}
want := []string{"CAP_DAC_READ_SEARCH", "CAP_SYS_ADMIN"}
if securityEvent.Host == nil || securityEvent.Host.Event != "capset" ||
!slices.Equal(securityEvent.Host.Capabilities, want) {
t.Fatalf("unexpected security event: %#v", securityEvent)
}
}
func TestDecodeSecuritySampleProducesModuleLoad(t *testing.T) {
raw := syscallEventRaw{
PID: 42, PPID: 7, UID: 0, SyscallID: hostFinitModule,
Args: [6]uint64{5},
}
var sample bytes.Buffer
if err := binary.Write(&sample, binary.NativeEndian, raw); err != nil {
t.Fatal(err)
}
securityEvent, err := decodeSecuritySample(sample.Bytes())
if err != nil {
t.Fatal(err)
}
if securityEvent.Host == nil || securityEvent.Host.Event != "module_load" || securityEvent.pathDirFD != 5 {
t.Fatalf("unexpected security event: %#v", securityEvent)
}
}
func TestDecodeSecuritySampleProducesFileWrite(t *testing.T) {
raw := syscallEventRaw{
PID: 42, PPID: 7, UID: 1000, SyscallID: hostFileWrite,
Args: [6]uint64{5, 128},
}
copyCString(raw.Comm[:], "python3")
var sample bytes.Buffer
if err := binary.Write(&sample, binary.NativeEndian, raw); err != nil {
t.Fatal(err)
}
securityEvent, err := decodeSecuritySample(sample.Bytes())
if err != nil {
t.Fatal(err)
}
if securityEvent.Host == nil || securityEvent.Host.Event != "file_write" ||
securityEvent.Host.Comm != "python3" || securityEvent.pathDirFD != 5 {
t.Fatalf("unexpected security event: %#v", securityEvent)
}
}
func TestDecodeSecuritySampleProducesNetworkEvents(t *testing.T) {
raw := syscallEventRaw{
PID: 42, PPID: 7, UID: 1000, SyscallID: hostTCPConnect,
Args: [6]uint64{unix.AF_INET, 4444, 0, 5},
}
copyCString(raw.Comm[:], "python3")
address := []byte{8, 8, 8, 8}
raw.Args[2] = uint64(binary.NativeEndian.Uint32(address))
var sample bytes.Buffer
if err := binary.Write(&sample, binary.NativeEndian, raw); err != nil {
t.Fatal(err)
}
securityEvent, err := decodeSecuritySample(sample.Bytes())
if err != nil {
t.Fatal(err)
}
if securityEvent.Host == nil || securityEvent.Host.Event != "tcp_connect" ||
securityEvent.Host.PeerIP != "8.8.8.8" || securityEvent.Host.PeerPort != 4444 ||
securityEvent.pathDirFD != 5 {
t.Fatalf("unexpected network event: %#v", securityEvent)
}
}
func TestDecodeSecuritySampleProducesAccept(t *testing.T) {
raw := syscallEventRaw{
PID: 42, PPID: 7, UID: 1000, SyscallID: hostAccept4,
Args: [6]uint64{0, 0, 0, 6},
}
copyCString(raw.Comm[:], "python3")
var sample bytes.Buffer
if err := binary.Write(&sample, binary.NativeEndian, raw); err != nil {
t.Fatal(err)
}
securityEvent, err := decodeSecuritySample(sample.Bytes())
if err != nil {
t.Fatal(err)
}
if securityEvent.Host == nil || securityEvent.Host.Event != "accept" || securityEvent.pathDirFD != 6 {
t.Fatalf("unexpected accept event: %#v", securityEvent)
}
}
func TestMonitoredSyscallsUsesPlatformConstants(t *testing.T) {
numbers := monitoredSyscalls()
if len(numbers) != 26+len(legacyPermissionSyscalls())+len(legacyNetworkSyscalls()) ||
numbers[unix.SYS_MEMFD_CREATE] != syscallMemfdCreate ||
numbers[unix.SYS_MPROTECT] != syscallMprotect || numbers[unix.SYS_SETUID] != hostSetuid {
t.Fatalf("unexpected syscall map: %#v", numbers)
}
for _, number := range []uint64{
unix.SYS_WRITE, unix.SYS_WRITEV, unix.SYS_PWRITE64,
unix.SYS_PWRITEV, unix.SYS_PWRITEV2,
} {
if numbers[number] != hostFileWrite {
t.Fatalf("write syscall %d maps to %d", number, numbers[number])
}
}
}
func TestResolveProcessPath(t *testing.T) {
procRoot := t.TempDir()
processDir := filepath.Join(procRoot, "42")
if err := os.Mkdir(processDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.Symlink("/etc/systemd", filepath.Join(processDir, "cwd")); err != nil {
t.Fatal(err)
}
if err := os.Mkdir(filepath.Join(processDir, "fd"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.Symlink("/etc", filepath.Join(processDir, "fd", "5")); err != nil {
t.Fatal(err)
}
if err := os.Symlink("socket:[12345]", filepath.Join(processDir, "fd", "6")); err != nil {
t.Fatal(err)
}
if err := os.Mkdir(filepath.Join(procRoot, "net"), 0o755); err != nil {
t.Fatal(err)
}
tcpTable := "sl local remote st tx rx tr tm retr uid timeout inode\n0: 0100007F:1F90 08080808:115C 01 0 0 0 0 0 12345\n"
if err := os.WriteFile(filepath.Join(procRoot, "net", "tcp"), []byte(tcpTable), 0o600); err != nil {
t.Fatal(err)
}
if got := resolveProcessPath(procRoot, 42, -100, "system/agent.service"); got != "/etc/systemd/system/agent.service" {
t.Fatalf("resolved path=%q", got)
}
if got := resolveProcessPath(procRoot, 42, 5, "cron.d/agent"); got != "/etc/cron.d/agent" {
t.Fatalf("dirfd-resolved path=%q", got)
}
if got := resolveProcessFD(procRoot, 42, 5); got != "/etc" {
t.Fatalf("fd-resolved path=%q", got)
}
if !isTCPSocket(procRoot, 42, 6) || isTCPSocket(procRoot, 42, 5) {
t.Fatal("TCP socket classification mismatch")
}
local, peer, ok := lookupTCPSocket(procRoot, 42, 6)
if !ok || local.IP != "127.0.0.1" || local.Port != 8080 || peer.IP != "8.8.8.8" || peer.Port != 4444 {
t.Fatalf("unexpected endpoints: local=%#v peer=%#v ok=%v", local, peer, ok)
}
if got := resolveProcessPath(procRoot, 99, -100, "relative"); got != "relative" {
t.Fatalf("unresolved path=%q", got)
}
}

View file

@ -0,0 +1,19 @@
// SPDX-License-Identifier: GPL-3.0-or-later
//go:build amd64
package ebpfsource
import "golang.org/x/sys/unix"
func legacyPermissionSyscalls() map[uint64]uint32 {
return map[uint64]uint32{
unix.SYS_CHMOD: hostChmod,
unix.SYS_CHOWN: hostChown,
unix.SYS_LCHOWN: hostLchown,
}
}
func legacyNetworkSyscalls() map[uint64]uint32 {
return map[uint64]uint32{unix.SYS_ACCEPT: hostAccept}
}

View file

@ -0,0 +1,13 @@
// SPDX-License-Identifier: GPL-3.0-or-later
//go:build !amd64
package ebpfsource
func legacyPermissionSyscalls() map[uint64]uint32 {
return nil
}
func legacyNetworkSyscalls() map[uint64]uint32 {
return nil
}