From e2a6952ffd97718dfa9c58ce85970882d5254aad Mon Sep 17 00:00:00 2001 From: Ben H Date: Mon, 4 May 2026 09:24:11 +0300 Subject: [PATCH 1/2] R0001 R1001 R1004: fall back to event.exepath in AP lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parse.get_exec_path(event.args, event.comm) returns argv[0] verbatim, which disagrees with the kernel-authoritative event.exepath that the recorder stores in the AP for two real-world patterns: - Relative argv[0] (e.g. "./python") — executes from cwd, AP records the resolved absolute path. - Empty argv[0] from fexecve / AT_EMPTY_PATH (e.g. modern libpam -> unix_chkpwd) — AP records the resolved exepath. In both cases the rule's lookup key (argv[0]) does not match the AP's storage key (exepath), so the rule fires on a legitimate, profiled exec. This patch adds a second AP lookup using event.exepath, with an empty- string guard so legacy AP entries with Path: "" can't false-suppress. The rule fires only when both lookups miss. Mirrors the recorder-side fix in node-agent PR #800. Signed-off-by: Ben H --- .../rule_test.go | 143 +++++++++++++++ .../unexpected-process-launched.yaml | 2 +- .../exec-binary-not-in-base-image.yaml | 3 +- .../rule_test.go | 97 ++++++++++ .../exec-from-mount.yaml | 2 +- pkg/rules/r1004-exec-from-mount/rule_test.go | 172 ++++++++++++++++++ 6 files changed, 416 insertions(+), 3 deletions(-) diff --git a/pkg/rules/r0001-unexpected-process-launched/rule_test.go b/pkg/rules/r0001-unexpected-process-launched/rule_test.go index dda72a2..91eadcf 100644 --- a/pkg/rules/r0001-unexpected-process-launched/rule_test.go +++ b/pkg/rules/r0001-unexpected-process-launched/rule_test.go @@ -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](), diff --git a/pkg/rules/r0001-unexpected-process-launched/unexpected-process-launched.yaml b/pkg/rules/r0001-unexpected-process-launched/unexpected-process-launched.yaml index 76d0a91..fee68cc 100644 --- a/pkg/rules/r0001-unexpected-process-launched/unexpected-process-launched.yaml +++ b/pkg/rules/r0001-unexpected-process-launched/unexpected-process-launched.yaml @@ -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 diff --git a/pkg/rules/r1001-exec-binary-not-in-base-image/exec-binary-not-in-base-image.yaml b/pkg/rules/r1001-exec-binary-not-in-base-image/exec-binary-not-in-base-image.yaml index de660dc..9133978 100644 --- a/pkg/rules/r1001-exec-binary-not-in-base-image/exec-binary-not-in-base-image.yaml +++ b/pkg/rules/r1001-exec-binary-not-in-base-image/exec-binary-not-in-base-image.yaml @@ -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 diff --git a/pkg/rules/r1001-exec-binary-not-in-base-image/rule_test.go b/pkg/rules/r1001-exec-binary-not-in-base-image/rule_test.go index a6df67c..50ea456 100644 --- a/pkg/rules/r1001-exec-binary-not-in-base-image/rule_test.go +++ b/pkg/rules/r1001-exec-binary-not-in-base-image/rule_test.go @@ -206,6 +206,103 @@ func TestR1001ExecBinaryNotInBaseImage(t *testing.T) { } } +// TestR1001ExepathFallback verifies the rule's exepath fallback for the AP lookup. +// See R0001 ExepathFallback test for full motivation. UpperLayer is set so the +// upper-layer clause is satisfied and the test isolates the AP lookup behavior. +func TestR1001ExepathFallback(t *testing.T) { + ruleSpec, err := common.LoadRuleFromYAML("exec-binary-not-in-base-image.yaml") + if err != nil { + t.Fatalf("Failed to load rule spec: %v", err) + } + + tests := []struct { + name string + event *utils.StructEvent + profile *v1beta1.ApplicationProfile + expectTrigger bool + description string + }{ + { + name: "relative argv[0] suppressed via exepath", + event: createTestExecEvent("test", "container123", "python", "/usr/bin/python3", "/", []string{"./python"}, true, false), + profile: createTestProfile("test", []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: createTestExecEvent("test", "container123", "unix_chkpwd", "/usr/sbin/unix_chkpwd", "/", []string{"", "root"}, true, false), + profile: createTestProfile("test", []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: createTestExecEvent("test", "container123", "foo", "", "/", []string{"/usr/bin/foo"}, true, false), + profile: createTestProfile("test", []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: createTestExecEvent("test", "container123", "newbinary", "/tmp/newbinary", "/", []string{"./newbinary"}, true, false), + profile: createTestProfile("test", []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("container123", &objectcache.WatchedContainerData{ + ContainerType: objectcache.Container, + ContainerInfos: map[objectcache.ContainerType][]objectcache.ContainerInfo{ + objectcache.Container: { + {Name: tt.event.Container}, + }, + }, + }) + + if tt.profile != nil { + objCache.SetApplicationProfile(tt.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} + + 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 TestR1001UpperLayerVariants(t *testing.T) { // Load rule spec ruleSpec, err := common.LoadRuleFromYAML("exec-binary-not-in-base-image.yaml") diff --git a/pkg/rules/r1004-exec-from-mount/exec-from-mount.yaml b/pkg/rules/r1004-exec-from-mount/exec-from-mount.yaml index 940cad1..9ca28ce 100644 --- a/pkg/rules/r1004-exec-from-mount/exec-from-mount.yaml +++ b/pkg/rules/r1004-exec-from-mount/exec-from-mount.yaml @@ -16,7 +16,7 @@ spec: uniqueId: "event.comm" ruleExpression: - eventType: "exec" - expression: "!ap.was_executed(event.containerId, parse.get_exec_path(event.args, event.comm)) && k8s.get_container_mount_paths(event.namespace, event.podName, event.containerName).exists(mount, event.exepath.startsWith(mount) || parse.get_exec_path(event.args, event.comm).startsWith(mount))" + expression: "!ap.was_executed(event.containerId, parse.get_exec_path(event.args, event.comm)) && (event.exepath == \"\" || !ap.was_executed(event.containerId, event.exepath)) && k8s.get_container_mount_paths(event.namespace, event.podName, event.containerName).exists(mount, event.exepath.startsWith(mount) || parse.get_exec_path(event.args, event.comm).startsWith(mount))" profileDependency: 1 profileDataRequired: execs: all diff --git a/pkg/rules/r1004-exec-from-mount/rule_test.go b/pkg/rules/r1004-exec-from-mount/rule_test.go index 49b077d..06f0922 100644 --- a/pkg/rules/r1004-exec-from-mount/rule_test.go +++ b/pkg/rules/r1004-exec-from-mount/rule_test.go @@ -217,3 +217,175 @@ func TestR1004ExecFromMount(t *testing.T) { t.Fatalf("Rule evaluation should have failed for exec from system path") } } + +// TestR1004ExepathFallback verifies the rule's exepath fallback for the AP lookup. +// All cases use mount paths /var/test1 or /var/test2 so the mount clause is satisfied; +// the test isolates the AP lookup behavior. See R0001 ExepathFallback test for full motivation. +func TestR1004ExepathFallback(t *testing.T) { + ruleSpec, err := common.LoadRuleFromYAML("exec-from-mount.yaml") + if err != nil { + t.Fatalf("Failed to load rule: %v", err) + } + + podSpec := &corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test", + VolumeMounts: []corev1.VolumeMount{ + {Name: "test-volume", MountPath: "/var/test1"}, + {Name: "test-volume-2", MountPath: "/var/test2"}, + }, + }, + }, + Volumes: []corev1.Volume{ + { + Name: "test-volume", + VolumeSource: corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{Path: "/var/test1"}, + }, + }, + { + Name: "test-volume-2", + VolumeSource: corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{Path: "/var/test2"}, + }, + }, + }, + } + + tests := []struct { + name string + event *utils.StructEvent + profileExecs []v1beta1.ExecCalls + expectTrigger bool + description string + }{ + { + name: "relative argv[0] suppressed via exepath (both under mount)", + event: &utils.StructEvent{ + Args: []string{"./python"}, + Comm: "python", + Container: "test", + ContainerID: "test-container", + EventType: utils.ExecveEventType, + ExePath: "/var/test1/python3", + Namespace: "test-namespace", + Pid: 1234, + Pod: "test-pod", + }, + profileExecs: []v1beta1.ExecCalls{ + {Path: "/var/test1/python3", Args: []string{"./python"}}, + }, + expectTrigger: false, + description: "argv[0]='./python' misses AP, but exepath '/var/test1/python3' matches", + }, + { + name: "empty argv[0] (fexecve) suppressed via exepath", + event: &utils.StructEvent{ + Args: []string{"", "root"}, + Comm: "unix_chkpwd", + Container: "test", + ContainerID: "test-container", + EventType: utils.ExecveEventType, + ExePath: "/var/test1/unix_chkpwd", + Namespace: "test-namespace", + Pid: 1234, + Pod: "test-pod", + }, + profileExecs: []v1beta1.ExecCalls{ + {Path: "/var/test1/unix_chkpwd", Args: []string{"", "root"}}, + }, + expectTrigger: false, + description: "argv[0]='' misses AP, but exepath '/var/test1/unix_chkpwd' matches", + }, + { + name: "empty exepath fallback guard — argv[0] match suppresses", + event: &utils.StructEvent{ + Args: []string{"/var/test2/foo"}, + Comm: "foo", + Container: "test", + ContainerID: "test-container", + EventType: utils.ExecveEventType, + ExePath: "", + Namespace: "test-namespace", + Pid: 1234, + Pod: "test-pod", + }, + profileExecs: []v1beta1.ExecCalls{ + {Path: "/var/test2/foo", Args: []string{"/var/test2/foo"}}, + }, + expectTrigger: false, + description: "exepath='' must not poll the AP; argv[0] '/var/test2/foo' alone suffices to suppress (mount clause satisfied via argv[0])", + }, + { + name: "both miss — rule still fires", + event: &utils.StructEvent{ + Args: []string{"./newbinary"}, + Comm: "newbinary", + Container: "test", + ContainerID: "test-container", + EventType: utils.ExecveEventType, + ExePath: "/var/test1/newbinary", + Namespace: "test-namespace", + Pid: 1234, + Pod: "test-pod", + }, + profileExecs: []v1beta1.ExecCalls{ + {Path: "/var/test1/something-else", Args: []string{"/var/test1/something-else"}}, + }, + expectTrigger: true, + description: "neither argv[0] nor exepath match AP; mount clause satisfied via exepath — 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-container", &objectcache.WatchedContainerData{ + ContainerType: objectcache.Container, + ContainerInfos: map[objectcache.ContainerType][]objectcache.ContainerInfo{ + objectcache.Container: { + {Name: "test"}, + }, + }, + }) + objCache.SetPodSpec(podSpec) + + 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) + } + + enrichedEvent := &events.EnrichedEvent{Event: tt.event} + + 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) + } + }) + } +} From feb98df062714b59da08475189276d4fbe089281 Mon Sep 17 00:00:00 2001 From: Ben H Date: Mon, 4 May 2026 10:09:58 +0300 Subject: [PATCH 2/2] R0007: also fall back to event.exepath in AP lookup Mirrors the same fix applied to R0001/R1001/R1004 in this branch: parse.get_exec_path(event.args, event.comm) returns argv[0] verbatim, which disagrees with the kernel-authoritative event.exepath that the recorder stores in the AP. The exec branch of R0007 has the same shape, so apply the same per-rule patch with empty-exepath guard. Suggested by Matthias on PR review. Signed-off-by: Ben H --- .../kubernetes-client-executed.yaml | 2 +- .../rule_test.go | 133 ++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) diff --git a/pkg/rules/r0007-kubernetes-client-executed/kubernetes-client-executed.yaml b/pkg/rules/r0007-kubernetes-client-executed/kubernetes-client-executed.yaml index 28cae9e..f3d4514 100644 --- a/pkg/rules/r0007-kubernetes-client-executed/kubernetes-client-executed.yaml +++ b/pkg/rules/r0007-kubernetes-client-executed/kubernetes-client-executed.yaml @@ -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 diff --git a/pkg/rules/r0007-kubernetes-client-executed/rule_test.go b/pkg/rules/r0007-kubernetes-client-executed/rule_test.go index 957d4b9..464a61a 100644 --- a/pkg/rules/r0007-kubernetes-client-executed/rule_test.go +++ b/pkg/rules/r0007-kubernetes-client-executed/rule_test.go @@ -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 {