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
143 changes: 143 additions & 0 deletions pkg/rules/r0001-unexpected-process-launched/rule_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,149 @@ func TestR0001UnexpectedProcessLaunched(t *testing.T) {
}
}

// TestR0001ExepathFallback verifies the rule's exepath fallback logic.
//
// parse.get_exec_path(event.args, event.comm) returns argv[0] verbatim, which
// can disagree with the kernel-authoritative event.exepath in two real cases:
// - relative argv[0] (e.g. "./python") — exepath is the resolved absolute path
// - empty argv[0] from fexecve / AT_EMPTY_PATH — exepath is the resolved path
//
// The rule now also checks event.exepath (with an empty-string guard) so the
// rule's AP lookup matches the recorder's storage key.
func TestR0001ExepathFallback(t *testing.T) {
ruleSpec, err := common.LoadRuleFromYAML("unexpected-process-launched.yaml")
if err != nil {
t.Fatalf("Failed to load rule: %v", err)
}

tests := []struct {
name string
event *utils.StructEvent
profileExecs []v1beta1.ExecCalls
expectTrigger bool
description string
}{
{
name: "relative argv[0] suppressed via exepath",
event: &utils.StructEvent{
Args: []string{"./python"},
Comm: "python",
Container: "test",
ContainerID: "test",
EventType: utils.ExecveEventType,
ExePath: "/usr/bin/python3",
Pcomm: "bash",
Pid: 1234,
},
profileExecs: []v1beta1.ExecCalls{
{Path: "/usr/bin/python3", Args: []string{"./python"}},
},
expectTrigger: false,
description: "argv[0]='./python' misses AP, but exepath '/usr/bin/python3' matches",
},
{
name: "empty argv[0] (fexecve) suppressed via exepath",
event: &utils.StructEvent{
Args: []string{"", "root"},
Comm: "unix_chkpwd",
Container: "test",
ContainerID: "test",
EventType: utils.ExecveEventType,
ExePath: "/usr/sbin/unix_chkpwd",
Pcomm: "sshd",
Pid: 1234,
},
profileExecs: []v1beta1.ExecCalls{
{Path: "/usr/sbin/unix_chkpwd", Args: []string{"", "root"}},
},
expectTrigger: false,
description: "argv[0]='' misses AP, but exepath '/usr/sbin/unix_chkpwd' matches",
},
{
name: "empty exepath fallback guard — argv[0] match suppresses",
event: &utils.StructEvent{
Args: []string{"/usr/bin/foo"},
Comm: "foo",
Container: "test",
ContainerID: "test",
EventType: utils.ExecveEventType,
ExePath: "",
Pcomm: "bash",
Pid: 1234,
},
profileExecs: []v1beta1.ExecCalls{
{Path: "/usr/bin/foo", Args: []string{"/usr/bin/foo"}},
},
expectTrigger: false,
description: "exepath='' must not poll the AP; argv[0] '/usr/bin/foo' alone suffices to suppress",
},
{
name: "both miss — rule still fires",
event: &utils.StructEvent{
Args: []string{"./newbinary"},
Comm: "newbinary",
Container: "test",
ContainerID: "test",
EventType: utils.ExecveEventType,
ExePath: "/tmp/newbinary",
Pcomm: "bash",
Pid: 1234,
},
profileExecs: []v1beta1.ExecCalls{
{Path: "/usr/bin/something-else", Args: []string{"/usr/bin/something-else"}},
},
expectTrigger: true,
description: "neither argv[0] nor exepath match the AP — must still fire",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
objCache := &objectcachev1.RuleObjectCacheMock{
ContainerIDToSharedData: maps.NewSafeMap[string, *objectcache.WatchedContainerData](),
}
objCache.SetSharedContainerData("test", &objectcache.WatchedContainerData{
ContainerType: objectcache.Container,
ContainerInfos: map[objectcache.ContainerType][]objectcache.ContainerInfo{
objectcache.Container: {
{Name: "test"},
},
},
})

profile := &v1beta1.ApplicationProfile{}
profile.Spec.Containers = append(profile.Spec.Containers, v1beta1.ApplicationProfileContainer{
Name: "test",
Execs: tt.profileExecs,
})
objCache.SetApplicationProfile(profile)

celEngine, err := celengine.NewCEL(objCache, config.Config{
CelConfigCache: cache.FunctionCacheConfig{
MaxSize: 1000,
TTL: 1 * time.Microsecond,
},
})
if err != nil {
t.Fatalf("Failed to create CEL engine: %v", err)
}

enrichedEvent := &events.EnrichedEvent{Event: tt.event}

// Sleep to ensure the cache from prior test runs is expired.
time.Sleep(1 * time.Millisecond)

triggered, err := celEngine.EvaluateRule(enrichedEvent, ruleSpec.Rules[0].Expressions.RuleExpression)
if err != nil {
t.Fatalf("Failed to evaluate rule: %v", err)
}
if triggered != tt.expectTrigger {
t.Errorf("expected trigger=%v, got trigger=%v. %s", tt.expectTrigger, triggered, tt.description)
}
})
}
}

func BenchmarkEvaluateRuleNative(b *testing.B) {
objCache := &objectcachev1.RuleObjectCacheMock{
ContainerIDToSharedData: maps.NewSafeMap[string, *objectcache.WatchedContainerData](),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ spec:
uniqueId: "event.comm + '_' + event.exepath"
ruleExpression:
- eventType: "exec"
expression: "!ap.was_executed(event.containerId, parse.get_exec_path(event.args, event.comm))"
expression: "!ap.was_executed(event.containerId, parse.get_exec_path(event.args, event.comm)) && (event.exepath == \"\" || !ap.was_executed(event.containerId, event.exepath))"
profileDependency: 0
profileDataRequired:
execs: all
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ spec:
uniqueId: "eventType == 'exec' ? 'exec_' + event.comm : 'network_' + event.dstAddr"
ruleExpression:
- eventType: "exec"
expression: "(event.comm == 'kubectl' || event.exepath.endsWith('/kubectl')) && !ap.was_executed(event.containerId, parse.get_exec_path(event.args, event.comm))"
expression: "(event.comm == 'kubectl' || event.exepath.endsWith('/kubectl')) && !ap.was_executed(event.containerId, parse.get_exec_path(event.args, event.comm)) && (event.exepath == \"\" || !ap.was_executed(event.containerId, event.exepath))"
- eventType: "network"
expression: "event.pktType == 'OUTGOING' && k8s.is_api_server_address(event.dstAddr) && !nn.was_address_in_egress(event.containerId, event.dstAddr)"
profileDependency: 0
Expand Down
133 changes: 133 additions & 0 deletions pkg/rules/r0007-kubernetes-client-executed/rule_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,139 @@ func TestR0007KubernetesClientExecuted(t *testing.T) {
}
}

func TestR0007ExepathFallback(t *testing.T) {
ruleSpec, err := common.LoadRuleFromYAML("kubernetes-client-executed.yaml")
if err != nil {
t.Fatalf("Failed to load rule: %v", err)
}

tests := []struct {
name string
event *utils.StructEvent
profileExecs []v1beta1.ExecCalls
expectTrigger bool
description string
}{
{
name: "relative argv[0] kubectl — exepath suppresses",
event: &utils.StructEvent{
Args: []string{"./kubectl", "get", "pods"},
Comm: "kubectl",
Container: "test",
ContainerID: "test",
EventType: utils.ExecveEventType,
ExePath: "/usr/local/bin/kubectl",
Pcomm: "bash",
Pid: 1234,
},
profileExecs: []v1beta1.ExecCalls{
{Path: "/usr/local/bin/kubectl", Args: []string{"./kubectl", "get", "pods"}},
},
expectTrigger: false,
description: "argv[0]='./kubectl' must not poll; exepath='/usr/local/bin/kubectl' matches AP entry",
},
{
name: "empty argv[0] (fexecve) kubectl — exepath suppresses",
event: &utils.StructEvent{
Args: []string{"", "get", "pods"},
Comm: "kubectl",
Container: "test",
ContainerID: "test",
EventType: utils.ExecveEventType,
ExePath: "/usr/bin/kubectl",
Pcomm: "bash",
Pid: 1234,
},
profileExecs: []v1beta1.ExecCalls{
{Path: "/usr/bin/kubectl", Args: []string{"kubectl", "get", "pods"}},
},
expectTrigger: false,
description: "fexecve produces empty argv[0]; exepath fallback must catch the AP entry",
},
{
name: "empty exepath fallback guard — argv[0] match suppresses",
event: &utils.StructEvent{
Args: []string{"kubectl", "get", "pods"},
Comm: "kubectl",
Container: "test",
ContainerID: "test",
EventType: utils.ExecveEventType,
ExePath: "",
Pcomm: "bash",
Pid: 1234,
},
profileExecs: []v1beta1.ExecCalls{
{Path: "kubectl", Args: []string{"kubectl", "get", "pods"}},
},
expectTrigger: false,
description: "exepath='' must not poll the AP; argv[0] 'kubectl' alone suffices to suppress",
},
{
name: "both miss — rule still fires",
event: &utils.StructEvent{
Args: []string{"./kubectl"},
Comm: "kubectl",
Container: "test",
ContainerID: "test",
EventType: utils.ExecveEventType,
ExePath: "/tmp/kubectl",
Pcomm: "bash",
Pid: 1234,
},
profileExecs: nil,
expectTrigger: true,
description: "neither argv[0] nor exepath in AP — rule must still fire",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
objCache := &objectcachev1.RuleObjectCacheMock{
ContainerIDToSharedData: maps.NewSafeMap[string, *objectcache.WatchedContainerData](),
}
objCache.SetSharedContainerData("test", &objectcache.WatchedContainerData{
ContainerType: objectcache.Container,
ContainerInfos: map[objectcache.ContainerType][]objectcache.ContainerInfo{
objectcache.Container: {{Name: "test"}},
},
})

if tt.profileExecs != nil {
profile := &v1beta1.ApplicationProfile{
Spec: v1beta1.ApplicationProfileSpec{
Containers: []v1beta1.ApplicationProfileContainer{
{Name: "test", Execs: tt.profileExecs},
},
},
}
objCache.SetApplicationProfile(profile)
}

celEngine, err := celengine.NewCEL(objCache, config.Config{
CelConfigCache: cache.FunctionCacheConfig{
MaxSize: 1000,
TTL: 1 * time.Microsecond,
},
})
if err != nil {
t.Fatalf("Failed to create CEL engine: %v", err)
}

time.Sleep(1 * time.Millisecond)

enrichedEvent := &events.EnrichedEvent{Event: tt.event}
triggered, err := celEngine.EvaluateRule(enrichedEvent, ruleSpec.Rules[0].Expressions.RuleExpression)
if err != nil {
t.Fatalf("Failed to evaluate rule: %v", err)
}
if triggered != tt.expectTrigger {
t.Errorf("%s: expected trigger=%v, got=%v. %s",
tt.name, tt.expectTrigger, triggered, tt.description)
}
})
}
}

func TestR0007KubernetesClientExecutedNetwork(t *testing.T) {
ruleSpec, err := common.LoadRuleFromYAML("kubernetes-client-executed.yaml")
if err != nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ spec:
expression: >
(event.upperlayer == true ||
event.pupperlayer == true) &&
!ap.was_executed(event.containerId, parse.get_exec_path(event.args, event.comm))
!ap.was_executed(event.containerId, parse.get_exec_path(event.args, event.comm)) &&
(event.exepath == "" || !ap.was_executed(event.containerId, event.exepath))
profileDependency: 1
profileDataRequired:
execs: all
Expand Down
Loading
Loading