diff --git a/pkg/containerprofilemanager/v1/event_reporting.go b/pkg/containerprofilemanager/v1/event_reporting.go index 997065da3..077875fe1 100644 --- a/pkg/containerprofilemanager/v1/event_reporting.go +++ b/pkg/containerprofilemanager/v1/event_reporting.go @@ -32,17 +32,33 @@ func (cpm *ContainerProfileManager) ReportCapability(containerID, capability str cpm.logEventError(err, "capability", containerID) } +// resolveExecPath derives the path to record for an exec event. It is kept +// symmetric with the rule-side resolver in +// pkg/rulemanager/cel/libraries/parse/parse.go (parse.get_exec_path): prefer +// the kernel-authoritative exepath, then argv[0] when non-empty, then comm. +// Using args[0] unconditionally produces an empty Path when the syscall has +// an empty pathname (fexecve / execveat AT_EMPTY_PATH — the libpam helper +// invocation pattern), while the rule-side resolver falls back to comm — +// leaving the AP entry unreachable to ap.was_executed and producing spurious +// "Unexpected process launched" alerts. +func resolveExecPath(exepath, comm string, args []string) string { + if exepath != "" { + return exepath + } + if len(args) > 0 && args[0] != "" { + return args[0] + } + return comm +} + // ReportFileExec reports a file execution event for a container func (cpm *ContainerProfileManager) ReportFileExec(containerID string, event utils.ExecEvent) { err := cpm.withContainer(containerID, func(data *containerData) (int, error) { if data.execs == nil { data.execs = &maps.SafeMap[string, []string]{} } - path := event.GetComm() args := event.GetArgs() - if len(args) > 0 { - path = args[0] - } + path := resolveExecPath(event.GetExePath(), event.GetComm(), args) // Use SHA256 hash of the exec to identify it uniquely execIdentifier := utils.CalculateSHA256FileExecHash(path, args) diff --git a/pkg/containerprofilemanager/v1/event_reporting_test.go b/pkg/containerprofilemanager/v1/event_reporting_test.go new file mode 100644 index 000000000..ee38683d5 --- /dev/null +++ b/pkg/containerprofilemanager/v1/event_reporting_test.go @@ -0,0 +1,57 @@ +package containerprofilemanager + +import "testing" + +func TestResolveExecPath(t *testing.T) { + tests := []struct { + name string + exepath string + comm string + args []string + want string + }{ + { + name: "exepath present (canonical exec)", + exepath: "/usr/sbin/unix_chkpwd", + comm: "unix_chkpwd", + args: []string{"/usr/sbin/unix_chkpwd", "root"}, + want: "/usr/sbin/unix_chkpwd", + }, + { + name: "fexecve / execveat AT_EMPTY_PATH — pathname empty, argv[0] non-empty", + exepath: "", + comm: "unix_chkpwd", + args: []string{"unix_chkpwd", "root"}, + want: "unix_chkpwd", + }, + { + name: "fexecve with empty argv[0] (older PAM convention)", + exepath: "", + comm: "unix_chkpwd", + args: []string{"", "root"}, + want: "unix_chkpwd", + }, + { + name: "no exepath, no args — fall back to comm", + exepath: "", + comm: "some_proc", + args: nil, + want: "some_proc", + }, + { + name: "exepath wins even when argv[0] disagrees (argv[0] spoofing)", + exepath: "/usr/bin/curl", + comm: "curl", + args: []string{"sshd", "-i"}, + want: "/usr/bin/curl", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := resolveExecPath(tt.exepath, tt.comm, tt.args) + if got != tt.want { + t.Errorf("resolveExecPath(%q, %q, %v) = %q, want %q", tt.exepath, tt.comm, tt.args, got, tt.want) + } + }) + } +}