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
82 changes: 64 additions & 18 deletions admission/rules/v1/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,24 @@ import (
"k8s.io/client-go/kubernetes"
)

// GetControllerDetails returns the kind, name, namespace, and node name of the controller that owns the pod.
func GetControllerDetails(event admission.Attributes, clientset kubernetes.Interface) (string, string, string, string, error) {
// GetControllerDetails returns the pod and controller details (pod, kind, name, namespace, uid, and node name).
// The workload UID is captured during owner resolution to avoid duplicate API calls.
func GetControllerDetails(event admission.Attributes, clientset kubernetes.Interface) (*corev1.Pod, string, string, string, string, string, error) {
podName, namespace := event.GetName(), event.GetNamespace()

if podName == "" || namespace == "" {
return "", "", "", "", fmt.Errorf("invalid pod details from admission event")
return nil, "", "", "", "", "", fmt.Errorf("invalid pod details from admission event")
}

pod, err := GetPodDetails(clientset, podName, namespace)
if err != nil {
return "", "", "", "", fmt.Errorf("failed to get pod details: %w", err)
return nil, "", "", "", "", "", fmt.Errorf("failed to get pod details: %w", err)
}

workloadKind, workloadName, workloadNamespace := ExtractPodOwner(pod, clientset)
workloadKind, workloadName, workloadNamespace, workloadUID := ExtractPodOwner(pod, clientset)
nodeName := pod.Spec.NodeName

return workloadKind, workloadName, workloadNamespace, nodeName, nil
return pod, workloadKind, workloadName, workloadNamespace, workloadUID, nodeName, nil
}

// GetPodDetails returns the pod details from the Kubernetes API server.
Expand All @@ -40,39 +41,43 @@ func GetPodDetails(clientset kubernetes.Interface, podName, namespace string) (*
return pod, nil
}

// ExtractPodOwner returns the kind, name, and namespace of the controller that owns the pod.
func ExtractPodOwner(pod *corev1.Pod, clientset kubernetes.Interface) (string, string, string) {
// ExtractPodOwner returns the kind, name, namespace, and UID of the controller that owns the pod.
// The UID is captured from OwnerReferences or during resolution to avoid duplicate API calls.
func ExtractPodOwner(pod *corev1.Pod, clientset kubernetes.Interface) (string, string, string, string) {
for _, ownerRef := range pod.OwnerReferences {
switch ownerRef.Kind {
case "ReplicaSet":
return resolveReplicaSet(ownerRef, pod.Namespace, clientset)
case "Job":
return resolveJob(ownerRef, pod.Namespace, clientset)
case "StatefulSet", "DaemonSet":
return ownerRef.Kind, ownerRef.Name, pod.Namespace
return ownerRef.Kind, ownerRef.Name, pod.Namespace, string(ownerRef.UID)
}
}
return "", "", ""
return "", "", "", ""
}

// resolveReplicaSet returns the kind, name, and namespace of the controller that owns the replica set.
func resolveReplicaSet(ownerRef metav1.OwnerReference, namespace string, clientset kubernetes.Interface) (string, string, string) {
// resolveReplicaSet returns the kind, name, namespace, and UID of the controller that owns the replica set.
// If the ReplicaSet is owned by a Deployment, returns the Deployment's details; otherwise returns the ReplicaSet's details.
func resolveReplicaSet(ownerRef metav1.OwnerReference, namespace string, clientset kubernetes.Interface) (string, string, string, string) {
rs, err := clientset.AppsV1().ReplicaSets(namespace).Get(context.TODO(), ownerRef.Name, metav1.GetOptions{})
if err == nil && len(rs.OwnerReferences) > 0 && rs.OwnerReferences[0].Kind == "Deployment" {
return "Deployment", rs.OwnerReferences[0].Name, namespace
return "Deployment", rs.OwnerReferences[0].Name, namespace, string(rs.OwnerReferences[0].UID)
}
return "ReplicaSet", ownerRef.Name, namespace
// If no Deployment parent or GET failed, use ReplicaSet's UID from the original ownerRef
return "ReplicaSet", ownerRef.Name, namespace, string(ownerRef.UID)
}

// resolveJob resolves the owner of a Kubernetes Job resource.
// It checks if the given Job is owned by a CronJob, and if so, it returns the CronJob's details.
// It checks if the given Job is owned by a CronJob, and if so, it returns the CronJob's details including UID.
// Otherwise, it returns the Job's details.
func resolveJob(ownerRef metav1.OwnerReference, namespace string, clientset kubernetes.Interface) (string, string, string) {
func resolveJob(ownerRef metav1.OwnerReference, namespace string, clientset kubernetes.Interface) (string, string, string, string) {
job, err := clientset.BatchV1().Jobs(namespace).Get(context.TODO(), ownerRef.Name, metav1.GetOptions{})
if err == nil && len(job.OwnerReferences) > 0 && job.OwnerReferences[0].Kind == "CronJob" {
return "CronJob", job.OwnerReferences[0].Name, namespace
return "CronJob", job.OwnerReferences[0].Name, namespace, string(job.OwnerReferences[0].UID)
}
return "Job", ownerRef.Name, namespace
// If no CronJob parent or GET failed, use Job's UID from the original ownerRef
return "Job", ownerRef.Name, namespace, string(ownerRef.UID)
}

// GetContainerNameFromExecToPodEvent returns the container name from the admission event for exec operations.
Expand All @@ -98,3 +103,44 @@ func GetContainerNameFromExecToPodEvent(event admission.Attributes) (string, err

return podExecOptions.Container, nil
}

// GetContainerID returns the container ID for the given container name from the pod status.
// It checks regular containers, init containers, and ephemeral containers.
// When containerName is empty, falls back to the first container (matching Kubernetes default behavior).
// Returns an empty string if the container is not found or pod is nil.
func GetContainerID(pod *corev1.Pod, containerName string) string {
if pod == nil {
return ""
}

// If containerName is empty, Kubernetes defaults to the first container
if containerName == "" {
if len(pod.Status.ContainerStatuses) > 0 {
return pod.Status.ContainerStatuses[0].ContainerID
}
return ""
}

// Check regular containers
for _, cs := range pod.Status.ContainerStatuses {
if cs.Name == containerName {
return cs.ContainerID
}
}

// Check init containers
for _, cs := range pod.Status.InitContainerStatuses {
if cs.Name == containerName {
return cs.ContainerID
}
}

// Check ephemeral containers (debug containers)
for _, cs := range pod.Status.EphemeralContainerStatuses {
if cs.Name == containerName {
return cs.ContainerID
}
}

return ""
}
8 changes: 6 additions & 2 deletions admission/rules/v1/r2000_exec_to_pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ func (rule *R2000ExecToPod) ProcessEvent(event admission.Attributes, access obje

client := access.GetClientset()

workloadKind, workloadName, workloadNamespace, nodeName, err := GetControllerDetails(event, client)
pod, workloadKind, workloadName, workloadNamespace, workloadUID, nodeName, err := GetControllerDetails(event, client)
if err != nil {
logger.L().Error("Failed to get parent workload details", helpers.Error(err))
logger.L().Error("Failed to get pod details", helpers.Error(err))
return nil
}

Expand All @@ -83,6 +83,8 @@ func (rule *R2000ExecToPod) ProcessEvent(event admission.Attributes, access obje
containerName = ""
}

containerID := GetContainerID(pod, containerName)

cmdline, err := getCommandLine(event.GetObject().(*unstructured.Unstructured))
if err != nil {
logger.L().Error("Failed to get command line from exec to pod event", helpers.Error(err))
Expand Down Expand Up @@ -132,8 +134,10 @@ func (rule *R2000ExecToPod) ProcessEvent(event admission.Attributes, access obje
WorkloadName: workloadName,
WorkloadNamespace: workloadNamespace,
WorkloadKind: workloadKind,
WorkloadUID: workloadUID,
NodeName: nodeName,
ContainerName: containerName,
ContainerID: containerID,
},
RuleID: R2000ID,
RuntimeProcessDetails: apitypes.ProcessTree{
Expand Down
50 changes: 50 additions & 0 deletions admission/rules/v1/r2000_exec_to_pod_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,58 @@ func TestR2000(t *testing.T) {
assert.Equal(t, "test-workload", result.GetRuntimeAlertK8sDetails().WorkloadName)
assert.Equal(t, "test-namespace", result.GetRuntimeAlertK8sDetails().WorkloadNamespace)
assert.Equal(t, "ReplicaSet", result.GetRuntimeAlertK8sDetails().WorkloadKind)
assert.Equal(t, "test-replicaset-uid-12345", result.GetRuntimeAlertK8sDetails().WorkloadUID)
assert.Equal(t, "test-node", result.GetRuntimeAlertK8sDetails().NodeName)
assert.Equal(t, "Exec to pod detected on pod test-pod", result.GetRuleAlert().RuleDescription)
assert.Equal(t, "test-pod", result.GetRuntimeAlertK8sDetails().PodName)
assert.Equal(t, "test-namespace", result.GetRuntimeAlertK8sDetails().Namespace)
assert.Equal(t, "containerd://abcdef1234567890", result.GetRuntimeAlertK8sDetails().ContainerID)
}

func TestR2000_EmptyContainerName(t *testing.T) {
// Test that empty container name defaults to first container (Kubernetes behavior)
event := admission.NewAttributesRecord(
&unstructured.Unstructured{
Object: map[string]interface{}{
"kind": "PodExecOptions",
"apiVersion": "v1",
"command": []interface{}{"sh"},
// No "container" field - should default to first container
"stdin": true,
"stdout": true,
"stderr": true,
"tty": true,
},
},
nil,
schema.GroupVersionKind{
Kind: "PodExecOptions",
},
"test-namespace",
"test-pod",
schema.GroupVersionResource{
Resource: "pods",
},
"exec",
admission.Create,
nil,
false,
&user.DefaultInfo{
Name: "test-user",
Groups: []string{"test-group"},
},
)

rule := CreateRuleR2000ExecToPod()
result := rule.ProcessEvent(event, objectcache.KubernetesCacheMockImpl{})

assert.NotNil(t, result)
// Container name should be empty (not specified)
assert.Equal(t, "", result.GetRuntimeAlertK8sDetails().ContainerName)
// But ContainerID should be resolved to first container
assert.Equal(t, "containerd://abcdef1234567890", result.GetRuntimeAlertK8sDetails().ContainerID)
// WorkloadUID should be populated even though container name was empty
assert.Equal(t, "test-replicaset-uid-12345", result.GetRuntimeAlertK8sDetails().WorkloadUID)
assert.Equal(t, "test-workload", result.GetRuntimeAlertK8sDetails().WorkloadName)
assert.Equal(t, "ReplicaSet", result.GetRuntimeAlertK8sDetails().WorkloadKind)
}
4 changes: 3 additions & 1 deletion admission/rules/v1/r2001_portforward.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,12 @@ func (rule *R2001PortForward) ProcessEvent(event admission.Attributes, access ob

client := access.GetClientset()

workloadKind, workloadName, workloadNamespace, nodeName, err := GetControllerDetails(event, client)
_, workloadKind, workloadName, workloadNamespace, workloadUID, nodeName, err := GetControllerDetails(event, client)
if err != nil {
logger.L().Error("Failed to get parent workload details", helpers.Error(err))
return nil
}

ruleFailure := GenericRuleFailure{
BaseRuntimeAlert: apitypes.BaseRuntimeAlert{
AlertName: rule.Name(),
Expand Down Expand Up @@ -111,6 +112,7 @@ func (rule *R2001PortForward) ProcessEvent(event admission.Attributes, access ob
WorkloadName: workloadName,
WorkloadNamespace: workloadNamespace,
WorkloadKind: workloadKind,
WorkloadUID: workloadUID,
NodeName: nodeName,
},
RuleID: R2001ID,
Expand Down
1 change: 1 addition & 0 deletions admission/rules/v1/r2001_portforward_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func TestR2001(t *testing.T) {
assert.Equal(t, "test-workload", result.GetRuntimeAlertK8sDetails().WorkloadName)
assert.Equal(t, "test-namespace", result.GetRuntimeAlertK8sDetails().WorkloadNamespace)
assert.Equal(t, "ReplicaSet", result.GetRuntimeAlertK8sDetails().WorkloadKind)
assert.Equal(t, "test-replicaset-uid-12345", result.GetRuntimeAlertK8sDetails().WorkloadUID)
assert.Equal(t, "test-node", result.GetRuntimeAlertK8sDetails().NodeName)
assert.Equal(t, "Port forward detected on pod test-pod", result.GetRuleAlert().RuleDescription)
assert.Equal(t, "test-pod", result.GetRuntimeAlertK8sDetails().PodName)
Expand Down
Loading
Loading