diff --git a/pkg/security/probe/actions.go b/pkg/security/probe/actions.go index e5628ee9100893..05cb80a50aea05 100644 --- a/pkg/security/probe/actions.go +++ b/pkg/security/probe/actions.go @@ -1,12 +1,10 @@ -//go:generate go run github.com/mailru/easyjson/easyjson -gen_build_flags=-mod=mod -no_std_marshalers -build_tags linux $GOFILE +//go:generate go run github.com/mailru/easyjson/easyjson -gen_build_flags=-mod=mod -no_std_marshalers $GOFILE // Unless explicitly stated otherwise all files in this repository are licensed // under the Apache License Version 2.0. // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2016-present Datadog, Inc. -//go:build linux - // Package probe holds probe related files package probe diff --git a/pkg/security/probe/actions_easyjson.go b/pkg/security/probe/actions_easyjson.go index 40f434be4162a5..7229f234dc6766 100644 --- a/pkg/security/probe/actions_easyjson.go +++ b/pkg/security/probe/actions_easyjson.go @@ -1,6 +1,3 @@ -//go:build linux -// +build linux - // Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. package probe diff --git a/pkg/security/probe/probe_linux.go b/pkg/security/probe/probe_linux.go index 11264b307dc388..33afe05d41458f 100644 --- a/pkg/security/probe/probe_linux.go +++ b/pkg/security/probe/probe_linux.go @@ -7,8 +7,6 @@ package probe import ( - "time" - "github.com/DataDog/datadog-agent/comp/core/workloadmeta" "github.com/DataDog/datadog-agent/pkg/security/config" "github.com/DataDog/datadog-agent/pkg/util/optional" @@ -19,8 +17,6 @@ const ( EBPFOrigin = "ebpf" // EBPFLessOrigin eBPF less origin EBPFLessOrigin = "ebpfless" - - defaultKillActionFlushDelay = 2 * time.Second ) // NewProbe instantiates a new runtime security agent probe diff --git a/pkg/security/probe/probe_windows.go b/pkg/security/probe/probe_windows.go index 4c668a8bb965a4..d2adc8827a207b 100644 --- a/pkg/security/probe/probe_windows.go +++ b/pkg/security/probe/probe_windows.go @@ -83,6 +83,9 @@ type WindowsProbe struct { // discarders discardedPaths *lru.Cache[string, struct{}] discardedBasenames *lru.Cache[string, struct{}] + + // actions + processKiller *ProcessKiller } type renameState struct { @@ -474,6 +477,9 @@ func (p *WindowsProbe) Start() error { } p.DispatchEvent(ev) + + // flush pending kill actions + p.processKiller.FlushPendingReports() } }() return p.pm.Start() @@ -535,11 +541,15 @@ func (p *WindowsProbe) handleProcessStop(ev *model.Event, stop *procmon.ProcessS log.Errorf("unable to resolve pid %d", pid) return false } + pce.ExitTime = time.Now() ev.Exit.Process = &pce.Process // use ProcessCacheEntry process context as process context ev.ProcessCacheEntry = pce ev.ProcessContext = &pce.ProcessContext + // update kill action reports + p.processKiller.HandleProcessExited(ev) + p.Resolvers.ProcessResolver.DequeueExited() return true } @@ -649,7 +659,7 @@ func (p *WindowsProbe) setProcessContext(pid uint32, event *model.Event) error { err := backoff.Retry(func() error { pce := p.Resolvers.ProcessResolver.GetEntry(pid) if pce == nil { - return fmt.Errorf("Could not resolve process for Process: %v", pid) + return fmt.Errorf("could not resolve process for Process: %v", pid) } event.ProcessCacheEntry = pce event.ProcessContext = &pce.ProcessContext @@ -661,7 +671,6 @@ func (p *WindowsProbe) setProcessContext(pid uint32, event *model.Event) error { // DispatchEvent sends an event to the probe event handler func (p *WindowsProbe) DispatchEvent(event *model.Event) { - traceEvent("Dispatching event %s", func() ([]byte, model.EventType, error) { eventJSON, err := serializers.MarshalEvent(event, nil) return eventJSON, event.GetEventType(), err @@ -672,7 +681,6 @@ func (p *WindowsProbe) DispatchEvent(event *model.Event) { // send event to specific event handlers, like the event monitor consumers, subsequently p.probe.sendEventToSpecificEventTypeHandlers(event) - } // Snapshot runs the different snapshot functions of the resolvers that @@ -803,6 +811,8 @@ func NewWindowsProbe(probe *Probe, config *config.Config, opts Opts) (*WindowsPr discardedPaths: discardedPaths, discardedBasenames: discardedBasenames, + + processKiller: NewProcessKiller(), } p.Resolvers, err = resolvers.NewResolvers(config, p.statsdClient, probe.scrubber) @@ -880,7 +890,22 @@ func (p *WindowsProbe) NewEvent() *model.Event { } // HandleActions executes the actions of a triggered rule -func (p *WindowsProbe) HandleActions(_ *eval.Context, _ *rules.Rule) {} +func (p *WindowsProbe) HandleActions(ctx *eval.Context, rule *rules.Rule) { + ev := ctx.Event.(*model.Event) + + for _, action := range rule.Definition.Actions { + if !action.IsAccepted(ctx) { + continue + } + + switch { + case action.Kill != nil: + p.processKiller.KillAndReport(action.Kill.Scope, action.Kill.Signal, ev, func(pid uint32, sig uint32) error { + return p.processKiller.KillFromUserspace(pid, sig, ev) + }) + } + } +} // AddDiscarderPushedCallback add a callback to the list of func that have to be called when a discarder is pushed to kernel func (p *WindowsProbe) AddDiscarderPushedCallback(_ DiscarderPushedCallback) {} diff --git a/pkg/security/probe/process_killer.go b/pkg/security/probe/process_killer.go index 5d600e8a93311c..ec27203540896f 100644 --- a/pkg/security/probe/process_killer.go +++ b/pkg/security/probe/process_killer.go @@ -3,21 +3,16 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2016-present Datadog, Inc. -//go:build linux +//go:build linux || windows // Package probe holds probe related files package probe import ( - "errors" - "fmt" "slices" "sync" - "syscall" "time" - psutil "github.com/shirou/gopsutil/v3/process" - "github.com/DataDog/datadog-agent/pkg/security/secl/model" "github.com/DataDog/datadog-agent/pkg/security/seclog" "github.com/DataDog/datadog-agent/pkg/security/utils" @@ -25,7 +20,7 @@ import ( ) const ( - userSpaceKillWithinMillis = 2000 + defaultKillActionFlushDelay = 2 * time.Second ) // ProcessKiller defines a process killer structure @@ -83,33 +78,6 @@ func (p *ProcessKiller) HandleProcessExited(event *model.Event) { }) } -// KillFromUserspace tries to kill from userspace -func (p *ProcessKiller) KillFromUserspace(pid uint32, sig uint32, ev *model.Event) error { - proc, err := psutil.NewProcess(int32(pid)) - if err != nil { - return errors.New("process not found in procfs") - } - - name, err := proc.Name() - if err != nil { - return errors.New("process not found in procfs") - } - - createdAt, err := proc.CreateTime() - if err != nil { - return errors.New("process not found in procfs") - } - evCreatedAt := ev.ProcessContext.ExecTime.UnixMilli() - - within := uint64(evCreatedAt) >= uint64(createdAt-userSpaceKillWithinMillis) && uint64(evCreatedAt) <= uint64(createdAt+userSpaceKillWithinMillis) - - if !within || ev.ProcessContext.Comm != name { - return fmt.Errorf("not sharing the same namespace: %s/%s", ev.ProcessContext.Comm, name) - } - - return syscall.Kill(int(pid), syscall.Signal(sig)) -} - // KillAndReport kill and report func (p *ProcessKiller) KillAndReport(scope string, signal string, ev *model.Event, killFnc func(pid uint32, sig uint32) error) { entry, exists := ev.ResolveProcessCacheEntry() @@ -117,16 +85,18 @@ func (p *ProcessKiller) KillAndReport(scope string, signal string, ev *model.Eve return } - var pids []uint32 - - if entry.ContainerID != "" && scope == "container" { - pids = entry.GetContainerPIDs() - scope = "container" - } else { - pids = []uint32{ev.ProcessContext.Pid} + switch scope { + case "container", "process": + default: scope = "process" } + pids, err := p.getPids(scope, ev, entry) + if err != nil { + log.Errorf("unable to kill: %s", err) + return + } + sig := model.SignalConstants[signal] killedAt := time.Now() diff --git a/pkg/security/probe/process_killer_linux.go b/pkg/security/probe/process_killer_linux.go new file mode 100644 index 00000000000000..521a841074ac2c --- /dev/null +++ b/pkg/security/probe/process_killer_linux.go @@ -0,0 +1,59 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +// Package probe holds probe related files +package probe + +import ( + "errors" + "fmt" + "syscall" + + psutil "github.com/shirou/gopsutil/v3/process" + + "github.com/DataDog/datadog-agent/pkg/security/secl/model" +) + +const ( + userSpaceKillWithinMillis = 2000 +) + +// KillFromUserspace tries to kill from userspace +func (p *ProcessKiller) KillFromUserspace(pid uint32, sig uint32, ev *model.Event) error { + proc, err := psutil.NewProcess(int32(pid)) + if err != nil { + return errors.New("process not found in procfs") + } + + name, err := proc.Name() + if err != nil { + return errors.New("process not found in procfs") + } + + createdAt, err := proc.CreateTime() + if err != nil { + return errors.New("process not found in procfs") + } + evCreatedAt := ev.ProcessContext.ExecTime.UnixMilli() + + within := uint64(evCreatedAt) >= uint64(createdAt-userSpaceKillWithinMillis) && uint64(evCreatedAt) <= uint64(createdAt+userSpaceKillWithinMillis) + + if !within || ev.ProcessContext.Comm != name { + return fmt.Errorf("not sharing the same namespace: %s/%s", ev.ProcessContext.Comm, name) + } + + return syscall.Kill(int(pid), syscall.Signal(sig)) +} + +func (p *ProcessKiller) getPids(scope string, ev *model.Event, entry *model.ProcessCacheEntry) ([]uint32, error) { + var pids []uint32 + + if entry.ContainerID != "" && scope == "container" { + pids = entry.GetContainerPIDs() + } else { + pids = []uint32{ev.ProcessContext.Pid} + } + return pids, nil +} diff --git a/pkg/security/probe/process_killer_windows.go b/pkg/security/probe/process_killer_windows.go new file mode 100644 index 00000000000000..25ff962f34136b --- /dev/null +++ b/pkg/security/probe/process_killer_windows.go @@ -0,0 +1,29 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +// Package probe holds probe related files +package probe + +import ( + "errors" + + "github.com/DataDog/datadog-agent/pkg/security/secl/model" + "github.com/DataDog/datadog-agent/pkg/util/winutil" +) + +// KillFromUserspace tries to kill from userspace +func (p *ProcessKiller) KillFromUserspace(pid uint32, sig uint32, _ *model.Event) error { + if sig != model.SIGKILL { + return nil + } + return winutil.KillProcess(int(pid), 0) +} + +func (p *ProcessKiller) getPids(scope string, ev *model.Event, _ *model.ProcessCacheEntry) ([]uint32, error) { + if scope == "container" { + return nil, errors.New("container scope not supported") + } + return []uint32{ev.ProcessContext.Pid}, nil +} diff --git a/pkg/security/secl/model/consts_other.go b/pkg/security/secl/model/consts_other.go index ccb5c2f6d5a6b2..a318b905acbecf 100644 --- a/pkg/security/secl/model/consts_other.go +++ b/pkg/security/secl/model/consts_other.go @@ -3,7 +3,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2016-present Datadog, Inc. -//go:build !linux +//go:build !linux && !windows package model diff --git a/pkg/security/secl/model/consts_windows.go b/pkg/security/secl/model/consts_windows.go new file mode 100644 index 00000000000000..3c368043861fcb --- /dev/null +++ b/pkg/security/secl/model/consts_windows.go @@ -0,0 +1,33 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +// Package model holds model related files +package model + +const ( + // SIGKILL id for the kill action + SIGKILL = iota + 1 +) + +var ( + errorConstants = map[string]int{} + openFlagsConstants = map[string]int{} + fileModeConstants = map[string]int{} + inodeModeConstants = map[string]int{} + // KernelCapabilityConstants list of kernel capabilities + KernelCapabilityConstants = map[string]uint64{} + unlinkFlagsConstants = map[string]int{} + ptraceConstants = map[string]uint32{} + ptraceArchConstants = map[string]uint32{} + protConstants = map[string]int{} + mmapFlagConstants = map[string]uint64{} + mmapFlagArchConstants = map[string]uint64{} + addressFamilyConstants = map[string]uint16{} + + // SignalConstants list of signals + SignalConstants = map[string]int{ + "SIGKILL": SIGKILL, + } +) diff --git a/pkg/security/utils/proc.go b/pkg/security/utils/proc_linux.go similarity index 99% rename from pkg/security/utils/proc.go rename to pkg/security/utils/proc_linux.go index 9e994240eade04..c5c612a372c27f 100644 --- a/pkg/security/utils/proc.go +++ b/pkg/security/utils/proc_linux.go @@ -3,8 +3,6 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2016-present Datadog, Inc. -//go:build linux - // Package utils holds utils related files package utils diff --git a/pkg/security/utils/proc_other.go b/pkg/security/utils/proc_other.go new file mode 100644 index 00000000000000..2e9429eb8ac34a --- /dev/null +++ b/pkg/security/utils/proc_other.go @@ -0,0 +1,18 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build !linux + +// Package utils holds utils related files +package utils + +import ( + "os" +) + +// Getpid returns the current process ID in the host namespace +func Getpid() uint32 { + return uint32(os.Getpid()) +} diff --git a/pkg/util/winutil/process.go b/pkg/util/winutil/process.go index 58bd218b46ae66..229b13a88fdfa1 100644 --- a/pkg/util/winutil/process.go +++ b/pkg/util/winutil/process.go @@ -399,3 +399,35 @@ func getProcessStartTimeAsNs(pid uint64) (uint64, error) { } return uint64(creation.Nanoseconds()), nil } + +// KillProcess kills the process with the given PID, supplying the given return code +func KillProcess(pid int, returnCode uint32) error { + /* + * Open the process with PROCESS_TERMINATE rights. This will fail + * if the process is not owned by the current user, or the user does not + * have admin rights + */ + h, err := windows.OpenProcess(windows.PROCESS_TERMINATE, false, uint32(pid)) + if err != nil { + return fmt.Errorf("Error opening process %v", err) + } + /* + * if the handle is successfully opened, must be closed to avoid handle leaks + */ + defer windows.Close(h) + /* + * terminate the process; the process will exit with the given return code + * + * See https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-terminateprocess for + * more information. + * + * A couple of notes: + * - This function will return immediately. The process itself will not be closed until all I/O is completed or cancelled + * - We could block and wait for the process to terminate, but this is not done here. + */ + err = windows.TerminateProcess(h, returnCode) + if err != nil { + return fmt.Errorf("Error terminating process %v", err) + } + return nil +}