Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions pkg/rulemanager/cel/libraries/process/process.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package process

import (
"errors"
"fmt"
"os"
"strings"

"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/prometheus/procfs"
)

var errProcessExited = errors.New("process exited before environment lookup")

// LD_PRELOAD_ENV_VARS contains the environment variables that can be used for LD_PRELOAD
var LD_PRELOAD_ENV_VARS = []string{
"LD_PRELOAD",
Expand Down Expand Up @@ -48,7 +53,7 @@ func (l *processLibrary) getProcessEnv(pid ref.Val) ref.Val {

envMap, err := GetProcessEnv(int(pidInt))
if err != nil {
return types.NewErr("failed to get process environment: %v", err)
return types.WrapErr(fmt.Errorf("failed to get process environment: %w", err))
}

// Convert map[string]string to map[string]interface{} for CEL
Expand All @@ -60,6 +65,17 @@ func (l *processLibrary) getProcessEnv(pid ref.Val) ref.Val {
return types.NewDynamicMap(types.DefaultTypeAdapter, result)
}

// processEnvOrEmpty converts an exited process to an empty environment after
// caching, so a missing process does not cache an empty map for a reused PID.
// This avoids expected evaluation errors; it cannot recover the exited process's
// environment, so environment-based rules may miss short-lived commands.
func processEnvOrEmpty(result ref.Val) ref.Val {
if err, ok := result.(*types.Err); ok && errors.Is(err, errProcessExited) {
return types.NewStringStringMap(types.DefaultTypeAdapter, map[string]string{})
}
return result
}

func (l *processLibrary) getLdHookVar(pid ref.Val) ref.Val {
pidUint, ok := pid.Value().(uint64)
if !ok {
Expand All @@ -81,7 +97,9 @@ func (l *processLibrary) getLdHookVar(pid ref.Val) ref.Val {
return types.String(envVar)
}

// GetProcessEnv retrieves the environment variables for a given process ID
// GetProcessEnv reads a process's live /proc/<pid>/environ at call time, not at
// exec-event capture time. Short-lived processes can exit before the read;
// their environment cannot be recovered from the exec event by this helper.
func GetProcessEnv(pid int) (map[string]string, error) {
fs, err := procfs.NewFS("/proc")
if err != nil {
Expand All @@ -90,11 +108,17 @@ func GetProcessEnv(pid int) (map[string]string, error) {

proc, err := fs.Proc(pid)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("%w: %w", errProcessExited, err)
}
return nil, err
}

env, err := proc.Environ()
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("%w: %w", errProcessExited, err)
}
return nil, err
}

Expand Down
69 changes: 69 additions & 0 deletions pkg/rulemanager/cel/libraries/process/process_env_exited_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package process

import (
"fmt"
"os"
"os/exec"
"testing"

"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/common/types/traits"
"github.com/kubescape/node-agent/pkg/config"
"github.com/kubescape/node-agent/pkg/rulemanager/cel/libraries/cache"
"github.com/stretchr/testify/require"
)

func TestProcessEnvExitedProcess(t *testing.T) {
cmd := exec.Command("true")
require.NoError(t, cmd.Run())
pid := cmd.Process.Pid
_, err := os.Stat(fmt.Sprintf("/proc/%d", pid))
require.ErrorIs(t, err, os.ErrNotExist)

env, err := cel.NewEnv(Process(config.Config{}))
require.NoError(t, err)
for index, expression := range []string{
fmt.Sprintf("size(process.get_process_env(%d)) == 0", pid),
fmt.Sprintf("'GLIBC_TUNABLES' in process.get_process_env(%d) && process.get_process_env(%d)['GLIBC_TUNABLES'].matches('glibc')", pid, pid),
} {
ast, issues := env.Compile(expression)
require.NoError(t, issues.Err())
program, err := env.Program(ast)
require.NoError(t, err)
result, _, err := program.Eval(map[string]any{})
require.NoError(t, err)
if index == 0 {
require.Equal(t, types.True, result)
} else {
require.Equal(t, types.False, result)
}
}
}

func TestProcessEnvPreservesOtherErrors(t *testing.T) {
for _, err := range []error{os.ErrPermission, os.ErrNotExist, fmt.Errorf("unexpected read failure")} {
result := types.WrapErr(fmt.Errorf("failed to get process environment: %w", err))
require.Same(t, result, processEnvOrEmpty(result))
}
}

func TestProcessEnvExitedResultIsNotCached(t *testing.T) {
functionCache := cache.NewFunctionCache(cache.FunctionCacheConfig{})
calls := 0
cached := functionCache.WithCache(func(...ref.Val) ref.Val {
calls++
if calls == 1 {
return types.WrapErr(fmt.Errorf("%w: %w", errProcessExited, os.ErrNotExist))
}
return types.NewStringStringMap(types.DefaultTypeAdapter, map[string]string{"GLIBC_TUNABLES": "glibc.malloc.check=1"})
}, "process.get_process_env")

pid := types.Int(123)
require.Equal(t, types.IntZero, processEnvOrEmpty(cached(pid)).(traits.Sizer).Size())
result := processEnvOrEmpty(cached(pid))
require.Equal(t, types.String("glibc.malloc.check=1"), result.(traits.Indexer).Get(types.String("GLIBC_TUNABLES")))
require.Equal(t, result, processEnvOrEmpty(cached(pid)))
require.Equal(t, 2, calls, "missing PIDs must be retried; successful environments must still be cached")
}
2 changes: 1 addition & 1 deletion pkg/rulemanager/cel/libraries/process/processlib.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func (l *processLibrary) Declarations() map[string][]cel.FunctionOpt {
return l.getProcessEnv(args[0])
}
cachedFunc := l.functionCache.WithCache(wrapperFunc, "process.get_process_env")
return cachedFunc(values[0])
return processEnvOrEmpty(cachedFunc(values[0]))
}),
),
},
Expand Down
Loading