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
114 changes: 114 additions & 0 deletions pkg/processtree/reparenting/reparenting_strategies_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
2 changes: 1 addition & 1 deletion pkg/processtree/reparenting/strategies/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
39 changes: 39 additions & 0 deletions pkg/processtree/reparenting/strategies/default_test.go
Original file line number Diff line number Diff line change
@@ -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))
})
}
}
34 changes: 34 additions & 0 deletions pkg/processtree/reparenting/strategies/fallback_test.go
Original file line number Diff line number Diff line change
@@ -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))
})
}
}
Loading