diff --git a/pkg/containerwatcher/v1/container_watcher.go b/pkg/containerwatcher/v1/container_watcher.go index 00683a3d..5cbaab0a 100644 --- a/pkg/containerwatcher/v1/container_watcher.go +++ b/pkg/containerwatcher/v1/container_watcher.go @@ -33,6 +33,8 @@ import ( tracerhardlinktype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/hardlink/types" tracerandomx "github.com/kubescape/node-agent/pkg/ebpf/gadgets/randomx/tracer" tracerandomxtype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/randomx/types" + tracerssh "github.com/kubescape/node-agent/pkg/ebpf/gadgets/ssh/tracer" + tracersshtype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/ssh/types" tracersymlink "github.com/kubescape/node-agent/pkg/ebpf/gadgets/symlink/tracer" tracersymlinktype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/symlink/types" "github.com/kubescape/node-agent/pkg/malwaremanager" @@ -55,6 +57,7 @@ const ( randomxTraceName = "trace_randomx" symlinkTraceName = "trace_symlink" hardlinkTraceName = "trace_hardlink" + sshTraceName = "trace_ssh" capabilitiesWorkerPoolSize = 1 execWorkerPoolSize = 2 openWorkerPoolSize = 8 @@ -63,6 +66,7 @@ const ( randomxWorkerPoolSize = 1 symlinkWorkerPoolSize = 1 hardlinkWorkerPoolSize = 1 + sshWorkerPoolSize = 1 ) type IGContainerWatcher struct { @@ -98,6 +102,7 @@ type IGContainerWatcher struct { randomxTracer *tracerandomx.Tracer symlinkTracer *tracersymlink.Tracer hardlinkTracer *tracerhardlink.Tracer + sshTracer *tracerssh.Tracer kubeIPInstance operators.OperatorInstance kubeNameInstance operators.OperatorInstance @@ -110,6 +115,7 @@ type IGContainerWatcher struct { randomxWorkerPool *ants.PoolWithFunc symlinkWorkerPool *ants.PoolWithFunc hardlinkWorkerPool *ants.PoolWithFunc + sshdWorkerPool *ants.PoolWithFunc capabilitiesWorkerChan chan *tracercapabilitiestype.Event execWorkerChan chan *tracerexectype.Event @@ -119,6 +125,7 @@ type IGContainerWatcher struct { randomxWorkerChan chan *tracerandomxtype.Event symlinkWorkerChan chan *tracersymlinktype.Event hardlinkWorkerChan chan *tracerhardlinktype.Event + sshWorkerChan chan *tracersshtype.Event preRunningContainersIDs mapset.Set[string] @@ -293,6 +300,18 @@ func CreateIGContainerWatcher(cfg config.Config, applicationProfileManager appli if err != nil { return nil, fmt.Errorf("creating hardlink worker pool: %w", err) } + // Create a ssh worker pool + sshWorkerPool, err := ants.NewPoolWithFunc(sshWorkerPoolSize, func(i interface{}) { + event := i.(tracersshtype.Event) + if event.K8s.ContainerName == "" { + return + } + metrics.ReportEvent(utils.SSHEventType) + ruleManager.ReportSSHEvent(event) + }) + if err != nil { + return nil, fmt.Errorf("creating ssh worker pool: %w", err) + } return &IGContainerWatcher{ // Configuration @@ -323,6 +342,7 @@ func CreateIGContainerWatcher(cfg config.Config, applicationProfileManager appli randomxWorkerPool: randomxWorkerPool, symlinkWorkerPool: symlinkWorkerPool, hardlinkWorkerPool: hardlinkWorkerPool, + sshdWorkerPool: sshWorkerPool, metrics: metrics, preRunningContainersIDs: preRunningContainers, @@ -335,6 +355,7 @@ func CreateIGContainerWatcher(cfg config.Config, applicationProfileManager appli randomxWorkerChan: make(chan *tracerandomxtype.Event, 5000), symlinkWorkerChan: make(chan *tracersymlinktype.Event, 1000), hardlinkWorkerChan: make(chan *tracerhardlinktype.Event, 1000), + sshWorkerChan: make(chan *tracersshtype.Event, 1000), // cache ruleBindingPodNotify: ruleBindingPodNotify, diff --git a/pkg/containerwatcher/v1/container_watcher_private.go b/pkg/containerwatcher/v1/container_watcher_private.go index b7e44377..bee90137 100644 --- a/pkg/containerwatcher/v1/container_watcher_private.go +++ b/pkg/containerwatcher/v1/container_watcher_private.go @@ -257,6 +257,12 @@ func (ch *IGContainerWatcher) startTracers() error { logger.L().Error("error starting hardlink tracing", helpers.Error(err)) return err } + + // NOTE: SSH tracing relies on the network tracer, so it must be started after the network tracer. + if err := ch.startSshTracing(); err != nil { + logger.L().Error("error starting ssh tracing", helpers.Error(err)) + return err + } } return nil @@ -322,6 +328,12 @@ func (ch *IGContainerWatcher) stopTracers() error { logger.L().Error("error stopping hardlink tracing", helpers.Error(err)) errs = errors.Join(errs, err) } + + // Stop ssh tracer + if err := ch.stopSshTracing(); err != nil { + logger.L().Error("error stopping ssh tracing", helpers.Error(err)) + errs = errors.Join(errs, err) + } } return errs diff --git a/pkg/containerwatcher/v1/ssh.go b/pkg/containerwatcher/v1/ssh.go new file mode 100644 index 00000000..a2976e50 --- /dev/null +++ b/pkg/containerwatcher/v1/ssh.go @@ -0,0 +1,78 @@ +package containerwatcher + +import ( + "fmt" + + tracerssh "github.com/kubescape/node-agent/pkg/ebpf/gadgets/ssh/tracer" + tracersshtype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/ssh/types" + + "github.com/inspektor-gadget/inspektor-gadget/pkg/container-collection/networktracer" + "github.com/inspektor-gadget/inspektor-gadget/pkg/types" + "github.com/kubescape/go-logger" + "github.com/kubescape/go-logger/helpers" +) + +func (ch *IGContainerWatcher) sshEventCallback(event *tracersshtype.Event) { + if event.Type == types.DEBUG { + return + } + + if isDroppedEvent(event.Type, event.Message) { + logger.L().Ctx(ch.ctx).Warning("ssh tracer got drop events - we may miss some realtime data", helpers.Interface("event", event), helpers.String("error", event.Message)) + return + } + + ch.containerCollection.EnrichByMntNs(&event.CommonData, event.MountNsID) + ch.containerCollection.EnrichByNetNs(&event.CommonData, event.NetNsID) + + ch.sshWorkerChan <- event +} + +func (ch *IGContainerWatcher) startSshTracing() error { + if err := ch.tracerCollection.AddTracer(sshTraceName, ch.containerSelector); err != nil { + return fmt.Errorf("adding tracer: %w", err) + } + + tracerSsh, err := tracerssh.NewTracer() + if err != nil { + return fmt.Errorf("creating tracer: %w", err) + } + go func() { + for event := range ch.sshWorkerChan { + _ = ch.sshdWorkerPool.Invoke(*event) + } + }() + + tracerSsh.SetSocketEnricherMap(ch.socketEnricher.SocketsMap()) + tracerSsh.SetEventHandler(ch.sshEventCallback) + + err = tracerSsh.RunWorkaround() + if err != nil { + return fmt.Errorf("running workaround: %w", err) + } + + ch.sshTracer = tracerSsh + + config := &networktracer.ConnectToContainerCollectionConfig[tracersshtype.Event]{ + Tracer: ch.sshTracer, + Resolver: ch.containerCollection, + Selector: ch.containerSelector, + Base: tracersshtype.Base, + } + + _, err = networktracer.ConnectToContainerCollection(config) + if err != nil { + return fmt.Errorf("creating tracer: %w", err) + } + + return nil +} + +func (ch *IGContainerWatcher) stopSshTracing() error { + // Stop ssh tracer + if err := ch.tracerCollection.RemoveTracer(sshTraceName); err != nil { + return fmt.Errorf("removing tracer: %w", err) + } + ch.sshTracer.Close() + return nil +} diff --git a/pkg/ebpf/gadgets/ssh/tracer/bpf/ssh.bpf.c b/pkg/ebpf/gadgets/ssh/tracer/bpf/ssh.bpf.c new file mode 100644 index 00000000..8fc637d5 --- /dev/null +++ b/pkg/ebpf/gadgets/ssh/tracer/bpf/ssh.bpf.c @@ -0,0 +1,108 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "../../../../include/macros.h" +#include "../../../../include/buffer.h" + +#define GADGET_TYPE_NETWORKING +#include "../../../../include/sockets-map.h" + +#include "ssh.h" + + +// Events map. +struct { + __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); + __uint(key_size, sizeof(__u32)); + __uint(value_size, sizeof(__u32)); +} events SEC(".maps"); + +// Empty event map. +struct { + __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); + __uint(max_entries, 1); + __type(key, __u32); + __type(value, struct event); +} empty_event SEC(".maps"); + +// we need this to make sure the compiler doesn't remove our struct. +const struct event *unusedevent __attribute__((unused)); + +SEC("socket") +int ssh_detector(struct __sk_buff *skb) { + // Check if it's an IP packet + if (skb->protocol != bpf_htons(ETH_P_IP)) + return 0; + + // Define the offset for IP header + int ip_offset = ETH_HLEN; + + // Read IP header + struct iphdr iph; + if (bpf_skb_load_bytes(skb, ip_offset, &iph, sizeof(iph)) < 0) + return 0; + + // Check if it's a TCP packet + if (iph.protocol != IPPROTO_TCP) + return 0; + + // Calculate TCP header offset + int tcp_offset = ip_offset + (iph.ihl * 4); + + // Read TCP header + struct tcphdr tcph; + if (bpf_skb_load_bytes(skb, tcp_offset, &tcph, sizeof(tcph)) < 0) + return 0; + + // Calculate payload offset + int payload_offset = tcp_offset + (tcph.doff * 4); + + // Read the first 4 bytes of the payload + char payload[SSH_SIG_LEN]; + if (bpf_skb_load_bytes(skb, payload_offset, payload, SSH_SIG_LEN) < 0) + return 0; + + // Check for SSH signature using memcmp + if (__builtin_memcmp(payload, SSH_SIGNATURE, SSH_SIG_LEN) == 0) { + struct event *event; + __u32 zero = 0; + event = bpf_map_lookup_elem(&empty_event, &zero); + if (!event) { + return 0; + } + + // Enrich event with process metadata + struct sockets_value *skb_val = gadget_socket_lookup(skb); + if (skb_val != NULL) { + event->netns = skb->cb[0]; // cb[0] initialized by dispatcher.bpf.c + event->mntns_id = skb_val->mntns; + event->pid = skb_val->pid_tgid >> 32; + event->uid = (__u32)(skb_val->uid_gid); + event->gid = (__u32)(skb_val->uid_gid >> 32); + __builtin_memcpy(&event->comm, skb_val->task, sizeof(event->comm)); + + event->src_ip = iph.saddr; + event->dst_ip = iph.daddr; + event->src_port = bpf_ntohs(tcph.source); + event->dst_port = bpf_ntohs(tcph.dest); + + event->timestamp = bpf_ktime_get_boot_ns(); + } + __u64 skb_len = skb->len; + bpf_perf_event_output(skb, &events, skb_len << 32 | BPF_F_CURRENT_CPU, event, sizeof(struct event)); + } + + return 0; +} + + +char LICENSE[] SEC("license") = "Dual BSD/GPL"; \ No newline at end of file diff --git a/pkg/ebpf/gadgets/ssh/tracer/bpf/ssh.h b/pkg/ebpf/gadgets/ssh/tracer/bpf/ssh.h new file mode 100644 index 00000000..b574e7ec --- /dev/null +++ b/pkg/ebpf/gadgets/ssh/tracer/bpf/ssh.h @@ -0,0 +1,33 @@ +#pragma once + +#include "../../../../include/types.h" + +#ifndef TASK_COMM_LEN +#define TASK_COMM_LEN 16 +#endif +#define INVALID_UID ((uid_t)-1) +// Defined in include/uapi/linux/magic.h +#define OVERLAYFS_SUPER_MAGIC 0x794c7630 +#ifndef PATH_MAX +#define PATH_MAX 4096 +#endif +#define SSH_SIGNATURE "SSH-" +#define SSH_SIG_LEN 4 +#define ETH_P_IP 0x0800 /* Internet Protocol packet */ +#define ETH_HLEN 14 + +struct event { + // Keep netns at the top: networktracer depends on it + __u32 netns; + + gadget_timestamp timestamp; + gadget_mntns_id mntns_id; + __u32 pid; + __u32 uid; + __u32 gid; + __u16 dst_port; + __u16 src_port; + __u32 dst_ip; + __u32 src_ip; + __u8 comm[TASK_COMM_LEN]; +}; diff --git a/pkg/ebpf/gadgets/ssh/tracer/ssh_bpfel.go b/pkg/ebpf/gadgets/ssh/tracer/ssh_bpfel.go new file mode 100644 index 00000000..0172cc7a --- /dev/null +++ b/pkg/ebpf/gadgets/ssh/tracer/ssh_bpfel.go @@ -0,0 +1,143 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build 386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 + +package tracer + +import ( + "bytes" + _ "embed" + "fmt" + "io" + + "github.com/cilium/ebpf" +) + +type sshEvent struct { + Netns uint32 + _ [4]byte + Timestamp uint64 + MntnsId uint64 + Pid uint32 + Uid uint32 + Gid uint32 + DstPort uint16 + SrcPort uint16 + DstIp uint32 + SrcIp uint32 + Comm [16]uint8 +} + +// loadSsh returns the embedded CollectionSpec for ssh. +func loadSsh() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_SshBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load ssh: %w", err) + } + + return spec, err +} + +// loadSshObjects loads ssh and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *sshObjects +// *sshPrograms +// *sshMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func loadSshObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := loadSsh() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// sshSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type sshSpecs struct { + sshProgramSpecs + sshMapSpecs +} + +// sshSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type sshProgramSpecs struct { + SshDetector *ebpf.ProgramSpec `ebpf:"ssh_detector"` +} + +// sshMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type sshMapSpecs struct { + EmptyEvent *ebpf.MapSpec `ebpf:"empty_event"` + Events *ebpf.MapSpec `ebpf:"events"` + GadgetHeap *ebpf.MapSpec `ebpf:"gadget_heap"` + GadgetSockets *ebpf.MapSpec `ebpf:"gadget_sockets"` +} + +// sshObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to loadSshObjects or ebpf.CollectionSpec.LoadAndAssign. +type sshObjects struct { + sshPrograms + sshMaps +} + +func (o *sshObjects) Close() error { + return _SshClose( + &o.sshPrograms, + &o.sshMaps, + ) +} + +// sshMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to loadSshObjects or ebpf.CollectionSpec.LoadAndAssign. +type sshMaps struct { + EmptyEvent *ebpf.Map `ebpf:"empty_event"` + Events *ebpf.Map `ebpf:"events"` + GadgetHeap *ebpf.Map `ebpf:"gadget_heap"` + GadgetSockets *ebpf.Map `ebpf:"gadget_sockets"` +} + +func (m *sshMaps) Close() error { + return _SshClose( + m.EmptyEvent, + m.Events, + m.GadgetHeap, + m.GadgetSockets, + ) +} + +// sshPrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to loadSshObjects or ebpf.CollectionSpec.LoadAndAssign. +type sshPrograms struct { + SshDetector *ebpf.Program `ebpf:"ssh_detector"` +} + +func (p *sshPrograms) Close() error { + return _SshClose( + p.SshDetector, + ) +} + +func _SshClose(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 ssh_bpfel.o +var _SshBytes []byte diff --git a/pkg/ebpf/gadgets/ssh/tracer/ssh_bpfel.o b/pkg/ebpf/gadgets/ssh/tracer/ssh_bpfel.o new file mode 100644 index 00000000..44b9eff2 Binary files /dev/null and b/pkg/ebpf/gadgets/ssh/tracer/ssh_bpfel.o differ diff --git a/pkg/ebpf/gadgets/ssh/tracer/tracer.go b/pkg/ebpf/gadgets/ssh/tracer/tracer.go new file mode 100644 index 00000000..0fb8e9a8 --- /dev/null +++ b/pkg/ebpf/gadgets/ssh/tracer/tracer.go @@ -0,0 +1,109 @@ +package tracer + +import ( + "context" + "encoding/binary" + "fmt" + "net/netip" + "unsafe" + + "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets" + "github.com/inspektor-gadget/inspektor-gadget/pkg/networktracer" + eventtypes "github.com/inspektor-gadget/inspektor-gadget/pkg/types" + "github.com/kubescape/node-agent/pkg/ebpf/gadgets/ssh/types" +) + +//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -no-global-types -target bpfel -cc clang -cflags "-g -O2 -Wall" -type event ssh bpf/ssh.bpf.c -- -I./bpf/ + +type Tracer struct { + *networktracer.Tracer[types.Event] + + cancel context.CancelFunc +} + +func NewTracer() (*Tracer, error) { + t := &Tracer{} + + if err := t.install(); err != nil { + t.Close() + return nil, fmt.Errorf("installing tracer: %w", err) + } + + return t, nil +} + +func (t *Tracer) Close() { + if t.cancel != nil { + t.cancel() + } + + if t.Tracer != nil { + t.Tracer.Close() + } +} + +func (t *Tracer) install() error { + networkTracer, err := networktracer.NewTracer[types.Event]() + if err != nil { + return fmt.Errorf("creating network tracer: %w", err) + } + t.Tracer = networkTracer + return nil +} + +func (t *Tracer) RunWorkaround() error { + if err := t.run(); err != nil { + t.Close() + return fmt.Errorf("running tracer: %w", err) + } + return nil +} + +func (t *Tracer) run() error { + spec, err := loadSsh() + if err != nil { + return fmt.Errorf("loading ebpf program: %w", err) + } + + if err := t.Tracer.Run(spec, types.Base, t.parseSSH); err != nil { + return fmt.Errorf("setting network tracer spec: %w", err) + } + + return nil +} + +func (t *Tracer) parseSSH(rawSample []byte, netns uint64) (*types.Event, error) { + bpfEvent := (*sshEvent)(unsafe.Pointer(&rawSample[0])) + + srcIP := [4]byte{} + binary.BigEndian.PutUint32(srcIP[:], bpfEvent.SrcIp) + src := netip.AddrFrom4(srcIP).String() + dstIP := [4]byte{} + binary.BigEndian.PutUint32(dstIP[:], bpfEvent.DstIp) + dst := netip.AddrFrom4(dstIP).String() + event := types.Event{ + Event: eventtypes.Event{ + Type: eventtypes.NORMAL, + Timestamp: gadgets.WallTimeFromBootTime(bpfEvent.Timestamp), + }, + WithMountNsID: eventtypes.WithMountNsID{MountNsID: bpfEvent.MntnsId}, + WithNetNsID: eventtypes.WithNetNsID{NetNsID: netns}, + SrcIP: src, + DstIP: dst, + SrcPort: bpfEvent.SrcPort, + DstPort: bpfEvent.DstPort, + Pid: bpfEvent.Pid, + Uid: bpfEvent.Uid, + Gid: bpfEvent.Gid, + Comm: gadgets.FromCString(bpfEvent.Comm[:]), + } + + return &event, nil +} + +type GadgetDesc struct{} + +func (g *GadgetDesc) NewInstance() (gadgets.Gadget, error) { + tracer := &Tracer{} + return tracer, nil +} diff --git a/pkg/ebpf/gadgets/ssh/types/types.go b/pkg/ebpf/gadgets/ssh/types/types.go new file mode 100644 index 00000000..65944120 --- /dev/null +++ b/pkg/ebpf/gadgets/ssh/types/types.go @@ -0,0 +1,33 @@ +package types + +import ( + "github.com/inspektor-gadget/inspektor-gadget/pkg/columns" + eventtypes "github.com/inspektor-gadget/inspektor-gadget/pkg/types" +) + +type Event struct { + eventtypes.Event + eventtypes.WithMountNsID + eventtypes.WithNetNsID + + Pid uint32 `json:"pid,omitempty" column:"pid,template:pid"` + Uid uint32 `json:"uid,omitempty" column:"uid,template:uid"` + Gid uint32 `json:"gid,omitempty" column:"gid,template:gid"` + Comm string `json:"comm,omitempty" column:"comm,template:comm"` + SrcPort uint16 `json:"src_port,omitempty" column:"src_port,template:src_port"` + DstPort uint16 `json:"dst_port,omitempty" column:"dst_port,template:dst_port"` + SrcIP string `json:"src_ip,omitempty" column:"src_ip,template:src_ip"` + DstIP string `json:"dst_ip,omitempty" column:"dst_ip,template:dst_ip"` +} + +func GetColumns() *columns.Columns[Event] { + sshColumns := columns.MustCreateColumns[Event]() + + return sshColumns +} + +func Base(ev eventtypes.Event) *Event { + return &Event{ + Event: ev, + } +} diff --git a/pkg/ebpf/include/sockets-map.h b/pkg/ebpf/include/sockets-map.h new file mode 100644 index 00000000..5ed51d87 --- /dev/null +++ b/pkg/ebpf/include/sockets-map.h @@ -0,0 +1,243 @@ +/* SPDX-License-Identifier: (GPL-2.0 WITH Linux-syscall-note) OR Apache-2.0 */ + +#ifndef SOCKETS_MAP_H +#define SOCKETS_MAP_H + +// The include below requires to include either +// or before. We can't include both because they +// are incompatible. Let the gadget choose which one to include. +#if !defined(__VMLINUX_H__) && !defined(_LINUX_TYPES_H) +#error "Include or before including this file." +#endif + +// Necessary for the SEC() definition +#include + +// This file is shared between the networking and tracing programs. +// Therefore, avoid includes that are specific to one of these types of programs. +// For example, don't include nor here. +// Redefine the constants we need but namespaced (SE_) so we don't pollute gadgets. + +#define SE_PACKET_HOST 0 +#define SE_ETH_HLEN 14 +#define SE_ETH_P_IP 0x0800 /* Internet Protocol packet */ +#define SE_ETH_P_IPV6 0x86DD /* IPv6 over bluebook */ +#define SE_AF_INET 2 /* Internet IP Protocol */ +#define SE_AF_INET6 10 /* IP version 6 */ + +#define SE_IPV6_HLEN 40 +#define SE_IPV6_NEXTHDR_OFFSET 6 // offsetof(struct ipv6hdr, nexthdr) + +#define SE_TCPHDR_DEST_OFFSET 2 // offsetof(struct tcphdr, dest); +#define SE_TCPHDR_SOURCE_OFFSET 0 // offsetof(struct tcphdr, source); +#define SE_UDPHDR_DEST_OFFSET 2 // offsetof(struct udphdr, dest); +#define SE_UDPHDR_SOURCE_OFFSET 0 // offsetof(struct udphdr, source); + +#define SE_NEXTHDR_HOP 0 /* Hop-by-hop option header. */ +#define SE_NEXTHDR_TCP 6 /* TCP segment. */ +#define SE_NEXTHDR_UDP 17 /* UDP message. */ +#define SE_NEXTHDR_ROUTING 43 /* Routing header. */ +#define SE_NEXTHDR_FRAGMENT 44 /* Fragmentation/reassembly header. */ +#define SE_NEXTHDR_AUTH 51 /* Authentication header. */ +#define SE_NEXTHDR_NONE 59 /* No next header */ +#define SE_NEXTHDR_DEST 60 /* Destination options header. */ + +#define SE_TASK_COMM_LEN 16 + +struct sockets_key { + __u32 netns; + __u16 family; + + // proto is IPPROTO_TCP(6) or IPPROTO_UDP(17) + __u8 proto; + __u16 port; +}; + +struct sockets_value { + __u64 mntns; + __u64 pid_tgid; + __u64 uid_gid; + char task[SE_TASK_COMM_LEN]; + __u64 sock; + __u64 deletion_timestamp; + char ipv6only; +}; + +#define MAX_SOCKETS 16384 +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, MAX_SOCKETS); + __type(key, struct sockets_key); + __type(value, struct sockets_value); +} gadget_sockets SEC(".maps"); + +#ifdef GADGET_TYPE_NETWORKING +static __always_inline struct sockets_value * +gadget_socket_lookup(const struct __sk_buff *skb) +{ + struct sockets_value *ret; + struct sockets_key key = { + 0, + }; + int l4_off; + __u16 h_proto; + int i; + long err; + + key.netns = skb->cb[0]; // cb[0] initialized by dispatcher.bpf.c + err = bpf_skb_load_bytes(skb, offsetof(struct ethhdr, h_proto), + &h_proto, sizeof(h_proto)); + if (err < 0) + return 0; + + switch (h_proto) { + case bpf_htons(SE_ETH_P_IP): + key.family = SE_AF_INET; + err = bpf_skb_load_bytes( + skb, SE_ETH_HLEN + offsetof(struct iphdr, protocol), + &key.proto, sizeof(key.proto)); + if (err < 0) + return 0; + + // An IPv4 header doesn't have a fixed size. The IHL field of a packet + // represents the size of the IP header in 32-bit words, so we need to + // multiply this value by 4 to get the header size in bytes. + __u8 ihl_byte; + err = bpf_skb_load_bytes(skb, SE_ETH_HLEN, &ihl_byte, + sizeof(ihl_byte)); + if (err < 0) + return 0; + struct iphdr *iph = (struct iphdr *)&ihl_byte; + __u8 ip_header_len = iph->ihl * 4; + l4_off = SE_ETH_HLEN + ip_header_len; + break; + + case bpf_htons(SE_ETH_P_IPV6): + key.family = SE_AF_INET6; + err = bpf_skb_load_bytes( + skb, SE_ETH_HLEN + SE_IPV6_NEXTHDR_OFFSET, + &key.proto, sizeof(key.proto)); + if (err < 0) + return 0; + l4_off = SE_ETH_HLEN + SE_IPV6_HLEN; + +// Parse IPv6 extension headers +// Up to 6 extension headers can be chained. See ipv6_ext_hdr(). +#pragma unroll + for (i = 0; i < 6; i++) { + __u8 nextproto; + __u8 off; + + // TCP or UDP found + if (key.proto == SE_NEXTHDR_TCP || + key.proto == SE_NEXTHDR_UDP) + break; + + err = bpf_skb_load_bytes(skb, l4_off, &nextproto, + sizeof(nextproto)); + if (err < 0) + return 0; + + // Unfortunately, each extension header has a different way to calculate the header length. + // Support the ones defined in ipv6_ext_hdr(). See ipv6_skip_exthdr(). + switch (key.proto) { + case SE_NEXTHDR_FRAGMENT: + // No hdrlen in the fragment header + l4_off += 8; + break; + case SE_NEXTHDR_AUTH: + // See ipv6_authlen() + err = bpf_skb_load_bytes(skb, l4_off + 1, &off, + sizeof(off)); + if (err < 0) + return 0; + l4_off += 4 * (off + 2); + break; + case SE_NEXTHDR_HOP: + case SE_NEXTHDR_ROUTING: + case SE_NEXTHDR_DEST: + // See ipv6_optlen() + err = bpf_skb_load_bytes(skb, l4_off + 1, &off, + sizeof(off)); + if (err < 0) + return 0; + l4_off += 8 * (off + 1); + break; + case SE_NEXTHDR_NONE: + // Nothing more in the packet. Not even TCP or UDP. + return 0; + default: + // Unknown header + return 0; + } + key.proto = nextproto; + } + break; + + default: + return 0; + } + + int off = l4_off; + switch (key.proto) { + case IPPROTO_TCP: + if (skb->pkt_type == SE_PACKET_HOST) + off += SE_TCPHDR_DEST_OFFSET; + else + off += SE_TCPHDR_SOURCE_OFFSET; + break; + case IPPROTO_UDP: + if (skb->pkt_type == SE_PACKET_HOST) + off += SE_UDPHDR_DEST_OFFSET; + else + off += SE_UDPHDR_SOURCE_OFFSET; + break; + default: + return 0; + } + + err = bpf_skb_load_bytes(skb, off, &key.port, sizeof(key.port)); + if (err < 0) + return 0; + key.port = bpf_ntohs(key.port); + + ret = bpf_map_lookup_elem(&gadget_sockets, &key); + if (ret) + return ret; + + // If a native socket was not found, try to find a dual-stack socket. + if (key.family == SE_AF_INET) { + key.family = SE_AF_INET6; + ret = bpf_map_lookup_elem(&gadget_sockets, &key); + if (ret && ret->ipv6only == 0) + return ret; + } + + return 0; +} +#endif + +#ifdef GADGET_TYPE_TRACING +static __always_inline struct sockets_value * +gadget_socket_lookup(const struct sock *sk, __u32 netns) +{ + struct sockets_key key = { + 0, + }; + key.netns = netns; + key.family = BPF_CORE_READ(sk, __sk_common.skc_family); + key.proto = BPF_CORE_READ_BITFIELD_PROBED(sk, sk_protocol); + if (key.proto != IPPROTO_TCP && key.proto != IPPROTO_UDP) + return 0; + + BPF_CORE_READ_INTO(&key.port, sk, __sk_common.skc_dport); + struct inet_sock *sockp = (struct inet_sock *)sk; + BPF_CORE_READ_INTO(&key.port, sockp, inet_sport); + // inet_sock.inet_sport is in network byte order + key.port = bpf_ntohs(key.port); + + return bpf_map_lookup_elem(&gadget_sockets, &key); +} +#endif + +#endif \ No newline at end of file diff --git a/pkg/ebpf/include/types.h b/pkg/ebpf/include/types.h index 78eb4ab7..cc49b63d 100644 --- a/pkg/ebpf/include/types.h +++ b/pkg/ebpf/include/types.h @@ -1,11 +1,71 @@ #pragma once -// This file contains inspektor gadget types that are needed untill we move to containerized build. +/* SPDX-License-Identifier: Apache-2.0 */ + +#ifndef __TYPES_H +#define __TYPES_H + +// Keep these types aligned with definitions in pkg/gadgets/run/tracer/tracer.go. + +// union defining either an IPv4 or IPv6 address +union gadget_ip_addr_t { + __u8 v6[16]; + __u32 v4; +}; + +// struct defining either an IPv4 or IPv6 L3 endpoint +struct gadget_l3endpoint_t { + union gadget_ip_addr_t addr_raw; + __u8 version; // 4 or 6 +}; + +// struct defining an L4 endpoint +struct gadget_l4endpoint_t { + union gadget_ip_addr_t addr_raw; + __u16 port; // L4 port in host byte order + __u16 proto; // IP protocol number + __u8 version; // 4 or 6 +}; // Inode id of a mount namespace. It's used to enrich the event in user space typedef __u64 gadget_mntns_id; +// Inode id of a network namespace. It's used to enrich the event in user space +typedef __u32 gadget_netns_id; + // gadget_timestamp is a type that represents the nanoseconds since the system boot. Gadgets can use // this type to provide a timestamp. The value contained must be the one returned by // bpf_ktime_get_boot_ns() and it's automatically converted by Inspektor Gadget to a human friendly // time. typedef __u64 gadget_timestamp; + +// gadget_signal is used to represent a unix signal. A field is automatically added that contains the name +// as string. +typedef __u32 gadget_signal; + +// gadget_errno is used to represent a unix errno. A field is automatically added that contains the name +// as string. +typedef __u32 gadget_errno; + +// gadget_uid is used to represent a uid. A field is automatically added that contains the corresponding user +// name on the host system +typedef __u32 gadget_uid; + +// gadget_gid is used to represent a uid. A field is automatically added that contains the corresponding group +// name on the host system +typedef __u32 gadget_gid; + +// gadget_syscall is used to represent a unix syscall. A field is automatically added that contains the name +// as string. +typedef __u64 gadget_syscall; + +typedef __u32 gadget_kernel_stack; + +// typedefs used for metrics +typedef __u32 gadget_counter__u32; +typedef __u64 gadget_counter__u64; +typedef __u32 gadget_gauge__u32; +typedef __u64 gadget_gauge__u64; +typedef __u32 gadget_histogram_slot__u32; +typedef __u64 gadget_histogram_slot__u64; + +#endif /* __TYPES_H */ diff --git a/pkg/ruleengine/v1/r1003_malicious_ssh_connection.go b/pkg/ruleengine/v1/r1003_malicious_ssh_connection.go index b4ceb5ce..ae62a3ce 100644 --- a/pkg/ruleengine/v1/r1003_malicious_ssh_connection.go +++ b/pkg/ruleengine/v1/r1003_malicious_ssh_connection.go @@ -3,51 +3,24 @@ package ruleengine import ( "fmt" "slices" - "strings" - "time" + "github.com/goradd/maps" "github.com/kubescape/node-agent/pkg/objectcache" "github.com/kubescape/node-agent/pkg/ruleengine" "github.com/kubescape/node-agent/pkg/utils" apitypes "github.com/armosec/armoapi-go/armotypes" - tracernetworktype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/network/types" - - traceropentype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/open/types" "github.com/kubescape/go-logger" + + tracersshtype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/ssh/types" ) const ( - R1003ID = "R1003" - R1003Name = "Malicious SSH Connection" - MaxTimeDiffInSeconds = 2 + R1003ID = "R1003" + R1003Name = "Malicious SSH Connection" ) -var SSHRelatedFiles = []string{ - "ssh_config", - "sshd_config", - "ssh_known_hosts", - "ssh_known_hosts2", - "ssh_config.d", - "sshd_config.d", - ".ssh", - "authorized_keys", - "authorized_keys2", - "known_hosts", - "known_hosts2", - "id_rsa", - "id_rsa.pub", - "id_dsa", - "id_dsa.pub", - "id_ecdsa", - "id_ecdsa.pub", - "id_ed25519", - "id_ed25519.pub", - "id_xmss", - "id_xmss.pub", -} - var R1003MaliciousSSHConnectionRuleDescriptor = RuleDescriptor{ ID: R1003ID, Name: R1003Name, @@ -55,7 +28,7 @@ var R1003MaliciousSSHConnectionRuleDescriptor = RuleDescriptor{ Tags: []string{"ssh", "connection", "port", "malicious"}, Priority: RulePriorityMed, Requirements: &RuleRequirements{ - EventTypes: []utils.EventType{utils.OpenEventType, utils.NetworkEventType}, + EventTypes: []utils.EventType{utils.SSHEventType}, }, RuleCreationFunc: func() ruleengine.RuleEvaluator { return CreateRuleR1003MaliciousSSHConnection() @@ -66,17 +39,13 @@ var _ ruleengine.RuleEvaluator = (*R1003MaliciousSSHConnection)(nil) type R1003MaliciousSSHConnection struct { BaseRule - accessRelatedFiles bool - sshInitiatorPid uint32 - configFileAccessTimeStamp int64 - allowedPorts []uint16 + allowedPorts []uint16 + requests maps.SafeMap[string, string] // Mapping of src IP to dst IP } func CreateRuleR1003MaliciousSSHConnection() *R1003MaliciousSSHConnection { - return &R1003MaliciousSSHConnection{accessRelatedFiles: false, - sshInitiatorPid: 0, - configFileAccessTimeStamp: 0, - allowedPorts: []uint16{22}, + return &R1003MaliciousSSHConnection{ + allowedPorts: []uint16{22}, } } func (rule *R1003MaliciousSSHConnection) Name() string { @@ -114,103 +83,50 @@ func (rule *R1003MaliciousSSHConnection) DeleteRule() { } func (rule *R1003MaliciousSSHConnection) ProcessEvent(eventType utils.EventType, event interface{}, objectCache objectcache.ObjectCache) ruleengine.RuleFailure { - if eventType != utils.OpenEventType && eventType != utils.NetworkEventType { + if eventType != utils.SSHEventType { return nil } - if eventType == utils.OpenEventType && !rule.accessRelatedFiles { - openEvent, ok := event.(*traceropentype.Event) - if !ok { - return nil - } else { - if IsSSHConfigFile(openEvent.FullPath) { - rule.accessRelatedFiles = true - rule.sshInitiatorPid = openEvent.Pid - rule.configFileAccessTimeStamp = int64(openEvent.Timestamp) - } - - return nil - } - } else if eventType == utils.NetworkEventType && rule.accessRelatedFiles { - networkEvent, ok := event.(*tracernetworktype.Event) - if !ok { - return nil - } - - nn := objectCache.NetworkNeighborhoodCache().GetNetworkNeighborhood(networkEvent.Runtime.ContainerID) - if nn == nil { - return nil - } + sshEvent := event.(*tracersshtype.Event) - timestampDiffInSeconds := calculateTimestampDiffInSeconds(int64(networkEvent.Timestamp), rule.configFileAccessTimeStamp) - if timestampDiffInSeconds > MaxTimeDiffInSeconds { - rule.accessRelatedFiles = false - rule.sshInitiatorPid = 0 - rule.configFileAccessTimeStamp = 0 + if !slices.Contains(rule.allowedPorts, sshEvent.DstPort) { + // Check if the event is a response to a request we have already seen. + if rule.requests.Has(sshEvent.DstIP) { return nil } - if networkEvent.Pid == rule.sshInitiatorPid && networkEvent.PktType == "OUTGOING" && networkEvent.Proto == "TCP" && !slices.Contains(rule.allowedPorts, networkEvent.Port) { - nnContainer, err := getContainerFromNetworkNeighborhood(nn, networkEvent.GetContainer()) - if err != nil { - return nil - } - for _, egress := range nnContainer.Egress { - for _, port := range egress.Ports { - if uint16(*port.Port) == networkEvent.Port { - return nil - } - } - } - rule.accessRelatedFiles = false - rule.sshInitiatorPid = 0 - rule.configFileAccessTimeStamp = 0 - - ruleFailure := GenericRuleFailure{ - BaseRuntimeAlert: apitypes.BaseRuntimeAlert{ - AlertName: rule.Name(), - InfectedPID: networkEvent.Pid, - FixSuggestions: "If this is a legitimate action, please add the port as a parameter to the binding of this rule", - Severity: R1003MaliciousSSHConnectionRuleDescriptor.Priority, - }, - RuntimeProcessDetails: apitypes.ProcessTree{ - ProcessTree: apitypes.Process{ - Comm: networkEvent.Comm, - Gid: &networkEvent.Gid, - PID: networkEvent.Pid, - Uid: &networkEvent.Uid, - }, - ContainerID: networkEvent.Runtime.ContainerID, - }, - TriggerEvent: networkEvent.Event, - RuleAlert: apitypes.RuleAlert{ - RuleDescription: fmt.Sprintf("SSH connection to disallowed port %d", networkEvent.Port), + rule.requests.Set(sshEvent.SrcIP, sshEvent.DstIP) + ruleFailure := GenericRuleFailure{ + BaseRuntimeAlert: apitypes.BaseRuntimeAlert{ + AlertName: rule.Name(), + InfectedPID: sshEvent.Pid, + FixSuggestions: "If this is a legitimate action, please add the port as a parameter to the binding of this rule", + Severity: R1003MaliciousSSHConnectionRuleDescriptor.Priority, + }, + RuntimeProcessDetails: apitypes.ProcessTree{ + ProcessTree: apitypes.Process{ + Comm: sshEvent.Comm, + Gid: &sshEvent.Gid, + PID: sshEvent.Pid, + Uid: &sshEvent.Uid, }, - RuntimeAlertK8sDetails: apitypes.RuntimeAlertK8sDetails{ - PodName: networkEvent.GetPod(), - }, - RuleID: rule.ID(), - } - - return &ruleFailure + ContainerID: sshEvent.Runtime.ContainerID, + }, + TriggerEvent: sshEvent.Event, + RuleAlert: apitypes.RuleAlert{ + RuleDescription: fmt.Sprintf("SSH connection to disallowed port %s:%d", sshEvent.DstIP, sshEvent.DstPort), + }, + RuntimeAlertK8sDetails: apitypes.RuntimeAlertK8sDetails{ + PodName: sshEvent.GetPod(), + }, + RuleID: rule.ID(), } + + return &ruleFailure } return nil } -func calculateTimestampDiffInSeconds(timestamp1 int64, timestamp2 int64) int64 { - return (timestamp1 - timestamp2) / int64(time.Second) -} - -func IsSSHConfigFile(path string) bool { - for _, sshFile := range SSHRelatedFiles { - if strings.Contains(path, sshFile) { - return true - } - } - return false -} - func (rule *R1003MaliciousSSHConnection) Requirements() ruleengine.RuleSpec { return &RuleRequirements{ EventTypes: R1003MaliciousSSHConnectionRuleDescriptor.Requirements.RequiredEventTypes(), diff --git a/pkg/ruleengine/v1/r1003_malicious_ssh_connection_test.go b/pkg/ruleengine/v1/r1003_malicious_ssh_connection_test.go index b527b565..628687e3 100644 --- a/pkg/ruleengine/v1/r1003_malicious_ssh_connection_test.go +++ b/pkg/ruleengine/v1/r1003_malicious_ssh_connection_test.go @@ -3,35 +3,16 @@ package ruleengine import ( "testing" + tracersshtype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/ssh/types" "github.com/kubescape/node-agent/pkg/utils" - tracernetworktype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/network/types" - traceropentype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/open/types" eventtypes "github.com/inspektor-gadget/inspektor-gadget/pkg/types" - "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" ) func TestR1003DisallowedSSHConnectionPort_ProcessEvent(t *testing.T) { rule := CreateRuleR1003MaliciousSSHConnection() - objCache := RuleObjectCacheMock{} - nn := objCache.NetworkNeighborhoodCache().GetNetworkNeighborhood("test") - if nn == nil { - nn = &v1beta1.NetworkNeighborhood{} - nn.Spec.Containers = append(nn.Spec.Containers, v1beta1.NetworkNeighborhoodContainer{ - Name: "test", - - Egress: []v1beta1.NetworkNeighbor{ - { - DNS: "test.com", - }, - }, - }) - - objCache.SetNetworkNeighborhood(nn) - } - // Test case 1: SSH connection to disallowed port - networkEvent := &tracernetworktype.Event{ + sshEvent := &tracersshtype.Event{ Event: eventtypes.Event{ Timestamp: 2, CommonData: eventtypes.CommonData{ @@ -48,71 +29,21 @@ func TestR1003DisallowedSSHConnectionPort_ProcessEvent(t *testing.T) { }, }, }, - PktType: "OUTGOING", - Proto: "TCP", - Port: 2222, - DstEndpoint: eventtypes.L3Endpoint{ - Addr: "1.1.1.1", - }, - Pid: 1, + SrcIP: "1.1.1.1", + DstIP: "2.2.2.2", + DstPort: 22, + SrcPort: 1234, } - openEvent := &traceropentype.Event{ - Event: eventtypes.Event{ - Timestamp: 1, - CommonData: eventtypes.CommonData{ - K8s: eventtypes.K8sMetadata{ - BasicK8sMetadata: eventtypes.BasicK8sMetadata{ - ContainerName: "test", - PodName: "test", - Namespace: "test", - }, - }, - Runtime: eventtypes.BasicRuntimeMetadata{ - ContainerID: "test", - ContainerName: "test", - }, - }, - }, - FullPath: "/etc/ssh/sshd_config", - Pid: 1, - } - rule.ProcessEvent(utils.OpenEventType, openEvent, &RuleObjectCacheMock{}) - failure := rule.ProcessEvent(utils.NetworkEventType, networkEvent, &objCache) - if failure == nil { - t.Errorf("Expected failure, but got nil") - } - - // Test case 2: SSH connection to allowed port - networkEvent.Port = 22 - failure = rule.ProcessEvent(utils.NetworkEventType, networkEvent, &objCache) + failure := rule.ProcessEvent(utils.SSHEventType, sshEvent, &RuleObjectCacheMock{}) if failure != nil { - t.Errorf("Expected failure to be nil, but got %v", failure) + t.Errorf("Expected nil since the SSH connection is to an allowed port, got %v", failure) } - // Test case 3: SSH connection to disallowed port, but not from SSH initiator - networkEvent.Port = 2222 - networkEvent.Pid = 2 - failure = rule.ProcessEvent(utils.NetworkEventType, networkEvent, &objCache) - if failure != nil { - t.Errorf("Expected failure to be nil, but got %v", failure) - } - - // Test case 4: SSH connection to disallowed port, but not from SSH initiator - networkEvent.Port = 2222 - networkEvent.Pid = 1 - networkEvent.Timestamp = 3 - failure = rule.ProcessEvent(utils.NetworkEventType, networkEvent, &objCache) - if failure != nil { - t.Errorf("Expected failure to be nil, but got %v", failure) - } - - // Test case 5: Time diff is greater than MaxTimeDiffInSeconds - networkEvent.Port = 2222 - networkEvent.Pid = 1 - networkEvent.Timestamp = 5 - failure = rule.ProcessEvent(utils.NetworkEventType, networkEvent, &objCache) - if failure != nil { - t.Errorf("Expected failure to be nil, but got %v", failure) + // Test disallowed port + sshEvent.DstPort = 1234 + failure = rule.ProcessEvent(utils.SSHEventType, sshEvent, &RuleObjectCacheMock{}) + if failure == nil { + t.Errorf("Expected failure since the SSH connection is to a disallowed port, got nil") } } diff --git a/pkg/ruleengine/v1/r1010_symlink_created_over_sensitive_file.go b/pkg/ruleengine/v1/r1010_symlink_created_over_sensitive_file.go index 7ea2f7aa..8a94d12c 100644 --- a/pkg/ruleengine/v1/r1010_symlink_created_over_sensitive_file.go +++ b/pkg/ruleengine/v1/r1010_symlink_created_over_sensitive_file.go @@ -1,6 +1,7 @@ package ruleengine import ( + "errors" "fmt" "strings" @@ -87,6 +88,12 @@ func (rule *R1010SymlinkCreatedOverSensitiveFile) ProcessEvent(eventType utils.E return nil } + if allowed, err := isSymLinkAllowed(symlinkEvent, objCache); err != nil { + return nil + } else if allowed { + return nil + } + for _, path := range rule.additionalPaths { if strings.HasPrefix(symlinkEvent.OldPath, path) { return &GenericRuleFailure{ @@ -129,3 +136,23 @@ func (rule *R1010SymlinkCreatedOverSensitiveFile) Requirements() ruleengine.Rule EventTypes: R1010SymlinkCreatedOverSensitiveFileRuleDescriptor.Requirements.RequiredEventTypes(), } } + +func isSymLinkAllowed(symlinkEvent *tracersymlinktype.Event, objCache objectcache.ObjectCache) (bool, error) { + ap := objCache.ApplicationProfileCache().GetApplicationProfile(symlinkEvent.Runtime.ContainerID) + if ap == nil { + return true, errors.New("application profile not found") + } + + appProfileExecList, err := getContainerFromApplicationProfile(ap, symlinkEvent.GetContainer()) + if err != nil { + return true, err + } + + for _, exec := range appProfileExecList.Execs { + if exec.Path == symlinkEvent.Comm { + return true, nil + } + } + + return false, nil +} diff --git a/pkg/ruleengine/v1/r1010_symlink_created_over_sensitive_file_test.go b/pkg/ruleengine/v1/r1010_symlink_created_over_sensitive_file_test.go index 42cec9b1..61689b8c 100644 --- a/pkg/ruleengine/v1/r1010_symlink_created_over_sensitive_file_test.go +++ b/pkg/ruleengine/v1/r1010_symlink_created_over_sensitive_file_test.go @@ -4,7 +4,9 @@ import ( "fmt" "testing" + eventtypes "github.com/inspektor-gadget/inspektor-gadget/pkg/types" "github.com/kubescape/node-agent/pkg/utils" + "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" tracersymlinktype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/symlink/types" ) @@ -16,14 +18,50 @@ func TestR1010SymlinkCreatedOverSensitiveFile(t *testing.T) { t.Errorf("Expected r to not be nil") } + objCache := RuleObjectCacheMock{} + profile := objCache.ApplicationProfileCache().GetApplicationProfile("test") + if profile == nil { + profile = &v1beta1.ApplicationProfile{ + Spec: v1beta1.ApplicationProfileSpec{ + Containers: []v1beta1.ApplicationProfileContainer{ + { + Name: "test", + Opens: []v1beta1.OpenCalls{ + { + Path: "/test", + Flags: []string{"O_RDONLY"}, + }, + }, + Execs: []v1beta1.ExecCalls{ + { + Path: "/usr/sbin/groupadd", + Args: []string{"test"}, + }, + }, + }, + }, + }, + } + objCache.SetApplicationProfile(profile) + } + // Create a symlink event e := &tracersymlinktype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + K8s: eventtypes.K8sMetadata{ + BasicK8sMetadata: eventtypes.BasicK8sMetadata{ + ContainerName: "test", + }, + }, + }, + }, Comm: "test", OldPath: "test", NewPath: "test", } - ruleResult := r.ProcessEvent(utils.SymlinkEventType, e, &RuleObjectCacheMock{}) + ruleResult := r.ProcessEvent(utils.SymlinkEventType, e, &objCache) if ruleResult != nil { fmt.Printf("ruleResult: %v\n", ruleResult) t.Errorf("Expected ruleResult to be nil since symlink path is not sensitive") @@ -34,7 +72,7 @@ func TestR1010SymlinkCreatedOverSensitiveFile(t *testing.T) { e.OldPath = "/etc/passwd" e.NewPath = "/etc/abc" - ruleResult = r.ProcessEvent(utils.SymlinkEventType, e, &RuleObjectCacheMock{}) + ruleResult = r.ProcessEvent(utils.SymlinkEventType, e, &objCache) if ruleResult == nil { fmt.Printf("ruleResult: %v\n", ruleResult) t.Errorf("Expected ruleResult to be Failure because of symlink is used over sensitive file") @@ -42,10 +80,22 @@ func TestR1010SymlinkCreatedOverSensitiveFile(t *testing.T) { } e.OldPath = "/etc/abc" - ruleResult = r.ProcessEvent(utils.SymlinkEventType, e, &RuleObjectCacheMock{}) + ruleResult = r.ProcessEvent(utils.SymlinkEventType, e, &objCache) if ruleResult != nil { fmt.Printf("ruleResult: %v\n", ruleResult) t.Errorf("Expected ruleResult to be nil since symlink is not used over sensitive file") return } + + // Test with whitelisted process + e.Comm = "/usr/sbin/groupadd" + e.OldPath = "/etc/passwd" + e.NewPath = "/etc/abc" + + ruleResult = r.ProcessEvent(utils.SymlinkEventType, e, &objCache) + if ruleResult != nil { + fmt.Printf("ruleResult: %v\n", ruleResult) + t.Errorf("Expected ruleResult to be nil since file is whitelisted and not sensitive") + return + } } diff --git a/pkg/ruleengine/v1/r1011_ld_preload_hook.go b/pkg/ruleengine/v1/r1011_ld_preload_hook.go index 912bef21..1a529d04 100644 --- a/pkg/ruleengine/v1/r1011_ld_preload_hook.go +++ b/pkg/ruleengine/v1/r1011_ld_preload_hook.go @@ -68,6 +68,11 @@ func (rule *R1011LdPreloadHook) handleExecEvent(execEvent *tracerexectype.Event, return nil } + // Check if the process is a MATLAB process and ignore it. + if execEvent.GetContainer() == "matlab" { + return nil + } + envVars, err := utils.GetProcessEnv(int(execEvent.Pid)) if err != nil { logger.L().Debug("Failed to get process environment variables", helpers.Error(err)) diff --git a/pkg/ruleengine/v1/r1012_hardlink_created_over_sensitive_file.go b/pkg/ruleengine/v1/r1012_hardlink_created_over_sensitive_file.go index a6634365..17334224 100644 --- a/pkg/ruleengine/v1/r1012_hardlink_created_over_sensitive_file.go +++ b/pkg/ruleengine/v1/r1012_hardlink_created_over_sensitive_file.go @@ -1,6 +1,7 @@ package ruleengine import ( + "errors" "fmt" "strings" @@ -87,6 +88,12 @@ func (rule *R1012HardlinkCreatedOverSensitiveFile) ProcessEvent(eventType utils. return nil } + if allowed, err := isHardLinkAllowed(hardlinkEvent, objCache); err != nil { + return nil + } else if allowed { + return nil + } + for _, path := range rule.additionalPaths { if strings.HasPrefix(hardlinkEvent.OldPath, path) { return &GenericRuleFailure{ @@ -129,3 +136,23 @@ func (rule *R1012HardlinkCreatedOverSensitiveFile) Requirements() ruleengine.Rul EventTypes: R1012HardlinkCreatedOverSensitiveFileRuleDescriptor.Requirements.RequiredEventTypes(), } } + +func isHardLinkAllowed(hardlinkEvent *tracerhardlinktype.Event, objCache objectcache.ObjectCache) (bool, error) { + ap := objCache.ApplicationProfileCache().GetApplicationProfile(hardlinkEvent.Runtime.ContainerID) + if ap == nil { + return true, errors.New("application profile not found") + } + + appProfileExecList, err := getContainerFromApplicationProfile(ap, hardlinkEvent.GetContainer()) + if err != nil { + return true, err + } + + for _, exec := range appProfileExecList.Execs { + if exec.Path == hardlinkEvent.Comm { + return true, nil + } + } + + return false, nil +} diff --git a/pkg/ruleengine/v1/r1012_hardlink_created_over_sensitive_file_test.go b/pkg/ruleengine/v1/r1012_hardlink_created_over_sensitive_file_test.go index 27394ca5..b106f1c1 100644 --- a/pkg/ruleengine/v1/r1012_hardlink_created_over_sensitive_file_test.go +++ b/pkg/ruleengine/v1/r1012_hardlink_created_over_sensitive_file_test.go @@ -4,7 +4,9 @@ import ( "fmt" "testing" + eventtypes "github.com/inspektor-gadget/inspektor-gadget/pkg/types" "github.com/kubescape/node-agent/pkg/utils" + "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" tracerhardlinktype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/hardlink/types" ) @@ -16,14 +18,50 @@ func TestR1012HardlinkCreatedOverSensitiveFile(t *testing.T) { t.Errorf("Expected r to not be nil") } + objCache := RuleObjectCacheMock{} + profile := objCache.ApplicationProfileCache().GetApplicationProfile("test") + if profile == nil { + profile = &v1beta1.ApplicationProfile{ + Spec: v1beta1.ApplicationProfileSpec{ + Containers: []v1beta1.ApplicationProfileContainer{ + { + Name: "test", + Opens: []v1beta1.OpenCalls{ + { + Path: "/test", + Flags: []string{"O_RDONLY"}, + }, + }, + Execs: []v1beta1.ExecCalls{ + { + Path: "/usr/sbin/groupadd", + Args: []string{"test"}, + }, + }, + }, + }, + }, + } + objCache.SetApplicationProfile(profile) + } + // Create a hardlink event e := &tracerhardlinktype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + K8s: eventtypes.K8sMetadata{ + BasicK8sMetadata: eventtypes.BasicK8sMetadata{ + ContainerName: "test", + }, + }, + }, + }, Comm: "test", OldPath: "test", NewPath: "test", } - ruleResult := r.ProcessEvent(utils.HardlinkEventType, e, &RuleObjectCacheMock{}) + ruleResult := r.ProcessEvent(utils.HardlinkEventType, e, &objCache) if ruleResult != nil { fmt.Printf("ruleResult: %v\n", ruleResult) t.Errorf("Expected ruleResult to be nil since hardlink path is not sensitive") @@ -33,8 +71,7 @@ func TestR1012HardlinkCreatedOverSensitiveFile(t *testing.T) { // Create a hardlink event with sensitive file path e.OldPath = "/etc/passwd" e.NewPath = "/etc/abc" - - ruleResult = r.ProcessEvent(utils.HardlinkEventType, e, &RuleObjectCacheMock{}) + ruleResult = r.ProcessEvent(utils.HardlinkEventType, e, &objCache) if ruleResult == nil { fmt.Printf("ruleResult: %v\n", ruleResult) t.Errorf("Expected ruleResult to be Failure because of hardlink is used over sensitive file") @@ -42,10 +79,22 @@ func TestR1012HardlinkCreatedOverSensitiveFile(t *testing.T) { } e.OldPath = "/etc/abc" - ruleResult = r.ProcessEvent(utils.HardlinkEventType, e, &RuleObjectCacheMock{}) + ruleResult = r.ProcessEvent(utils.HardlinkEventType, e, &objCache) if ruleResult != nil { fmt.Printf("ruleResult: %v\n", ruleResult) t.Errorf("Expected ruleResult to be nil since hardlink is not used over sensitive file") return } + + // Test with whitelisted process + e.Comm = "/usr/sbin/groupadd" + e.OldPath = "/etc/passwd" + e.NewPath = "/etc/abc" + + ruleResult = r.ProcessEvent(utils.HardlinkEventType, e, &objCache) + if ruleResult != nil { + fmt.Printf("ruleResult: %v\n", ruleResult) + t.Errorf("Expected ruleResult to be nil since file is whitelisted and not sensitive") + return + } } diff --git a/pkg/rulemanager/rule_manager_interface.go b/pkg/rulemanager/rule_manager_interface.go index 520fb383..52ae18c6 100644 --- a/pkg/rulemanager/rule_manager_interface.go +++ b/pkg/rulemanager/rule_manager_interface.go @@ -3,6 +3,7 @@ package rulemanager import ( tracerhardlinktype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/hardlink/types" tracerrandomxtype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/randomx/types" + tracersshtype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/ssh/types" tracersymlinktype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/symlink/types" containercollection "github.com/inspektor-gadget/inspektor-gadget/pkg/container-collection" @@ -11,6 +12,7 @@ import ( tracerexectype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/exec/types" tracernetworktype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/network/types" traceropentype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/open/types" + v1 "k8s.io/api/core/v1" ) @@ -25,6 +27,7 @@ type RuleManagerClient interface { ReportRandomxEvent(event tracerrandomxtype.Event) ReportSymlinkEvent(event tracersymlinktype.Event) ReportHardlinkEvent(event tracerhardlinktype.Event) + ReportSSHEvent(event tracersshtype.Event) HasApplicableRuleBindings(namespace, name string) bool HasFinalApplicationProfile(pod *v1.Pod) bool IsContainerMonitored(k8sContainerID string) bool diff --git a/pkg/rulemanager/rule_manager_mock.go b/pkg/rulemanager/rule_manager_mock.go index b8edc6b9..08d0cbd1 100644 --- a/pkg/rulemanager/rule_manager_mock.go +++ b/pkg/rulemanager/rule_manager_mock.go @@ -3,6 +3,7 @@ package rulemanager import ( tracerhardlinktype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/hardlink/types" tracerrandomxtype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/randomx/types" + tracersshtype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/ssh/types" tracersymlinktype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/symlink/types" containercollection "github.com/inspektor-gadget/inspektor-gadget/pkg/container-collection" @@ -63,6 +64,10 @@ func (r *RuleManagerMock) ReportHardlinkEvent(_ tracerhardlinktype.Event) { // noop } +func (r *RuleManagerMock) ReportSSHEvent(_ tracersshtype.Event) { + // noop +} + func (r *RuleManagerMock) HasApplicableRuleBindings(_, _ string) bool { return false } diff --git a/pkg/rulemanager/v1/rule_manager.go b/pkg/rulemanager/v1/rule_manager.go index 848bd250..92c3b3f4 100644 --- a/pkg/rulemanager/v1/rule_manager.go +++ b/pkg/rulemanager/v1/rule_manager.go @@ -28,6 +28,7 @@ import ( tracerhardlinktype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/hardlink/types" tracerrandomxtype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/randomx/types" + tracersshtype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/ssh/types" tracersymlinktype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/symlink/types" ruleenginetypes "github.com/kubescape/node-agent/pkg/ruleengine/types" @@ -423,6 +424,17 @@ func (rm *RuleManager) ReportHardlinkEvent(event tracerhardlinktype.Event) { rm.processEvent(utils.HardlinkEventType, &event, rules) } +func (rm *RuleManager) ReportSSHEvent(event tracersshtype.Event) { + if event.GetNamespace() == "" || event.GetPod() == "" { + logger.L().Error("RuleManager - failed to get namespace and pod name from ReportSSHEvent event") + return + } + + // list ssh rules + rules := rm.ruleBindingCache.ListRulesForPod(event.GetNamespace(), event.GetPod()) + rm.processEvent(utils.SSHEventType, &event, rules) +} + func (rm *RuleManager) processEvent(eventType utils.EventType, event interface{}, rules []ruleengine.RuleEvaluator) { for _, rule := range rules { if rule == nil { diff --git a/pkg/utils/events.go b/pkg/utils/events.go index 8198f837..5af8e2e0 100644 --- a/pkg/utils/events.go +++ b/pkg/utils/events.go @@ -12,5 +12,6 @@ const ( RandomXEventType SymlinkEventType HardlinkEventType + SSHEventType AllEventType )