diff --git a/pkg/processtree/reparenting/reparenting_strategies_test.go b/pkg/processtree/reparenting/reparenting_strategies_test.go new file mode 100644 index 0000000000..bc578c14b1 --- /dev/null +++ b/pkg/processtree/reparenting/reparenting_strategies_test.go @@ -0,0 +1,114 @@ +package reparenting + +import ( + "testing" + + "github.com/armosec/armoapi-go/armotypes" + "github.com/goradd/maps" + containerprocesstree "github.com/kubescape/node-agent/pkg/processtree/container" + "github.com/kubescape/node-agent/pkg/processtree/reparenting/strategies" + "github.com/stretchr/testify/require" +) + +type fakeStrategy struct { + name string + applicable bool + parentPID uint32 + checked bool +} + +func (f *fakeStrategy) Name() string { return f.name } + +func (f *fakeStrategy) IsApplicable(uint32, containerprocesstree.ContainerProcessTree, *maps.SafeMap[uint32, *armotypes.Process]) bool { + f.checked = true + return f.applicable +} + +func (f *fakeStrategy) GetNewParentPID(uint32, []*armotypes.Process, containerprocesstree.ContainerProcessTree, *maps.SafeMap[uint32, *armotypes.Process]) uint32 { + return f.parentPID +} + +func TestNewReparentingLogic(t *testing.T) { + rl, err := NewReparentingLogic() + require.NoError(t, err) + got := rl.GetStrategies() + require.Len(t, got, 3) + require.IsType(t, &strategies.ContainerStrategy{}, got[0]) + require.IsType(t, &strategies.FallBackStrategy{}, got[1]) + require.IsType(t, &strategies.DefaultStrategy{}, got[2], "the first strategy is listed last") +} + +func TestReparent_NoChildren(t *testing.T) { + first := &fakeStrategy{name: "first", applicable: true, parentPID: 42} + rl := &reparentingLogicImpl{firstStrategy: first} + for _, children := range [][]*armotypes.Process{nil, {}} { + newParent, err := rl.Reparent(100, children, nil, &maps.SafeMap[uint32, *armotypes.Process]{}) + require.NoError(t, err) + require.Zero(t, newParent) + } + require.False(t, first.checked, "no strategy must be consulted without children") +} + +func TestReparent_StrategySelection(t *testing.T) { + for _, tc := range []struct { + name string + first *fakeStrategy + strategies []*fakeStrategy + wantParent uint32 + wantErr bool + }{ + {"first strategy wins", &fakeStrategy{applicable: true, parentPID: 10}, []*fakeStrategy{{applicable: true, parentPID: 20}}, 10, false}, + {"first applicable in order wins", &fakeStrategy{}, []*fakeStrategy{{}, {applicable: true, parentPID: 20}, {applicable: true, parentPID: 30}}, 20, false}, + {"no applicable strategy", &fakeStrategy{}, []*fakeStrategy{{}}, 0, true}, + } { + t.Run(tc.name, func(t *testing.T) { + rl := &reparentingLogicImpl{firstStrategy: tc.first} + for _, s := range tc.strategies { + rl.AddStrategy(s) + } + newParent, err := rl.Reparent(100, []*armotypes.Process{{PID: 101, PPID: 100}}, nil, &maps.SafeMap[uint32, *armotypes.Process]{}) + require.Equal(t, tc.wantErr, err != nil) + require.Equal(t, tc.wantParent, newParent) + require.True(t, tc.first.checked) + if !tc.wantErr { + require.False(t, tc.strategies[len(tc.strategies)-1].checked, "strategies after the selected one must not be consulted") + } + }) + } +} + +func TestReparent_DefaultStrategies(t *testing.T) { + const parent, exiting, child = uint32(101), uint32(102), uint32(103) + for _, tc := range []struct { + name string + nodes map[uint32]*armotypes.Process + wantParent uint32 + }{ + {"tracked parent", map[uint32]*armotypes.Process{parent: {PID: parent, PPID: 1}, exiting: {PID: exiting, PPID: parent}}, parent}, + {"missing parent", map[uint32]*armotypes.Process{exiting: {PID: exiting, PPID: parent}}, 1}, + } { + t.Run(tc.name, func(t *testing.T) { + rl, err := NewReparentingLogic() + require.NoError(t, err) + processMap := &maps.SafeMap[uint32, *armotypes.Process]{} + for pid, proc := range tc.nodes { + processMap.Set(pid, proc) + } + // A nil container tree leaves only the default and fallback strategies. + newParent, err := rl.Reparent(exiting, []*armotypes.Process{{PID: child, PPID: exiting}}, nil, processMap) + require.NoError(t, err) + require.Equal(t, tc.wantParent, newParent) + }) + } +} + +func TestAddStrategy_GetStrategies(t *testing.T) { + first, a, b := &fakeStrategy{name: "first"}, &fakeStrategy{name: "a"}, &fakeStrategy{name: "b"} + rl := &reparentingLogicImpl{firstStrategy: first} + rl.AddStrategy(a) + rl.AddStrategy(b) + got := rl.GetStrategies() + require.Equal(t, []ReparentingStrategy{a, b, first}, got) + got[0] = b + require.Equal(t, []ReparentingStrategy{a, b, first}, rl.GetStrategies(), "callers must get a copy") +} diff --git a/pkg/processtree/reparenting/strategies/default.go b/pkg/processtree/reparenting/strategies/default.go index a44c827332..a9c7f91520 100644 --- a/pkg/processtree/reparenting/strategies/default.go +++ b/pkg/processtree/reparenting/strategies/default.go @@ -9,7 +9,7 @@ import ( type DefaultStrategy struct{} func (defs *DefaultStrategy) Name() string { - return "fallback" + return "default" } func (defs *DefaultStrategy) IsApplicable(exitingPID uint32, containerTree containerprocesstree.ContainerProcessTree, processMap *maps.SafeMap[uint32, *armotypes.Process]) bool { diff --git a/pkg/processtree/reparenting/strategies/default_test.go b/pkg/processtree/reparenting/strategies/default_test.go new file mode 100644 index 0000000000..8bb0591b10 --- /dev/null +++ b/pkg/processtree/reparenting/strategies/default_test.go @@ -0,0 +1,39 @@ +package strategies + +import ( + "testing" + + "github.com/armosec/armoapi-go/armotypes" + "github.com/goradd/maps" + "github.com/stretchr/testify/require" +) + +func TestDefaultStrategy_Name(t *testing.T) { + require.Equal(t, "default", (&DefaultStrategy{}).Name()) + require.NotEqual(t, (&FallBackStrategy{}).Name(), (&DefaultStrategy{}).Name()) +} + +func TestDefaultStrategy(t *testing.T) { + const parent, exiting, child = uint32(101), uint32(102), uint32(103) + for _, tc := range []struct { + name string + nodes map[uint32]*armotypes.Process + wantApplicable bool + wantParent uint32 + }{ + {"missing exiting process", map[uint32]*armotypes.Process{parent: {PID: parent, PPID: 1}}, false, 0}, + {"zero ppid", map[uint32]*armotypes.Process{exiting: {PID: exiting}}, false, 0}, + {"missing parent", map[uint32]*armotypes.Process{exiting: {PID: exiting, PPID: parent}}, false, 0}, + {"tracked parent", map[uint32]*armotypes.Process{parent: {PID: parent, PPID: 1}, exiting: {PID: exiting, PPID: parent}}, true, parent}, + } { + t.Run(tc.name, func(t *testing.T) { + processMap := &maps.SafeMap[uint32, *armotypes.Process]{} + for pid, proc := range tc.nodes { + processMap.Set(pid, proc) + } + strategy := &DefaultStrategy{} + require.Equal(t, tc.wantApplicable, strategy.IsApplicable(exiting, nil, processMap)) + require.Equal(t, tc.wantParent, strategy.GetNewParentPID(exiting, []*armotypes.Process{{PID: child, PPID: exiting}}, nil, processMap)) + }) + } +} diff --git a/pkg/processtree/reparenting/strategies/fallback_test.go b/pkg/processtree/reparenting/strategies/fallback_test.go new file mode 100644 index 0000000000..8d3bc3e840 --- /dev/null +++ b/pkg/processtree/reparenting/strategies/fallback_test.go @@ -0,0 +1,34 @@ +package strategies + +import ( + "testing" + + "github.com/armosec/armoapi-go/armotypes" + "github.com/goradd/maps" + containerprocesstree "github.com/kubescape/node-agent/pkg/processtree/container" + "github.com/stretchr/testify/require" +) + +func TestFallBackStrategy(t *testing.T) { + const parent, exiting, child = uint32(101), uint32(102), uint32(103) + for _, tc := range []struct { + name string + nodes map[uint32]*armotypes.Process + containerTree containerprocesstree.ContainerProcessTree + }{ + {"empty map", nil, nil}, + {"tracked parent", map[uint32]*armotypes.Process{parent: {PID: parent, PPID: 1}, exiting: {PID: exiting, PPID: parent}}, nil}, + {"under a container", map[uint32]*armotypes.Process{exiting: {PID: exiting, PPID: 100}}, &MockContainerProcessTree{shimPID: 100}}, + } { + t.Run(tc.name, func(t *testing.T) { + processMap := &maps.SafeMap[uint32, *armotypes.Process]{} + for pid, proc := range tc.nodes { + processMap.Set(pid, proc) + } + strategy := &FallBackStrategy{} + require.Equal(t, "fallback", strategy.Name()) + require.True(t, strategy.IsApplicable(exiting, tc.containerTree, processMap)) + require.Equal(t, uint32(1), strategy.GetNewParentPID(exiting, []*armotypes.Process{{PID: child, PPID: exiting}}, tc.containerTree, processMap)) + }) + } +}