From 1b0ffd3f6247bff5e6fab94f2c85626ce1ca543d Mon Sep 17 00:00:00 2001 From: itsvishalyadav Date: Wed, 11 Nov 2026 12:47:30 +0530 Subject: [PATCH] fix(exceptions): don't ignore label constraints on non-workload objects metadataHasException compared a designator's labels (and containerName) only for workload objects. For a RegoResponseVector base object, a host-sensor object or a cloud-provider object the constraint was skipped, so an exception scoped only by labels (attributes: {app: nginx}) matched every RBAC subject, host and cloud finding of the control. Such objects have no labels or containers to compare, so a designator that constrains them now doesn't match; a related object of a vector that carries the label still does. Co-Authored-By: Claude Opus 5.5 Signed-off-by: itsvishalyadav --- exceptions/exceptionprocessor.go | 32 +++++++----- exceptions/exceptionprocessor_test.go | 70 +++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 13 deletions(-) diff --git a/exceptions/exceptionprocessor.go b/exceptions/exceptionprocessor.go index 5e8d4a3c..7b54ea69 100644 --- a/exceptions/exceptionprocessor.go +++ b/exceptions/exceptionprocessor.go @@ -416,25 +416,31 @@ func (p *Processor) metadataHasException(workload workloadinterface.IMetadata, a } } - if isTypeWorkload(workload) { - allLabels := attributes.GetLabels() - containerName, hasContainerName := allLabels[identifiers.AttributeContainerName] + allLabels := attributes.GetLabels() + containerName, hasContainerName := allLabels[identifiers.AttributeContainerName] - // Build a label map with the attributes that are not Kubernetes labels stripped out, - // so they are not compared as such. - realLabels := stripNonLabelAttributes(allLabels) + // Build a label map with the attributes that are not Kubernetes labels stripped out, + // so they are not compared as such. + realLabels := stripNonLabelAttributes(allLabels) - if len(realLabels) > 0 { - if !p.compareLabels(workload, realLabels) && !p.compareAnnotations(workload, realLabels) { - return false // labels nor annotations do not match - } - } + if !isTypeWorkload(workload) { + // RegoResponseVector, host-sensor and cloud-provider objects have no labels, + // annotations or containers to compare, so a designator that constrains any of + // them cannot match. Skipping the comparison instead would widen a label-scoped + // exception to every such object. + return len(realLabels) == 0 && !hasContainerName + } - if hasContainerName && !p.compareContainerName(workload, containerName, failingContainerNames) { - return false // container name does not match + if len(realLabels) > 0 { + if !p.compareLabels(workload, realLabels) && !p.compareAnnotations(workload, realLabels) { + return false // labels nor annotations do not match } } + if hasContainerName && !p.compareContainerName(workload, containerName, failingContainerNames) { + return false // container name does not match + } + return true } diff --git a/exceptions/exceptionprocessor_test.go b/exceptions/exceptionprocessor_test.go index a8ddf6be..256972fe 100644 --- a/exceptions/exceptionprocessor_test.go +++ b/exceptions/exceptionprocessor_test.go @@ -2451,3 +2451,73 @@ func TestHasException_ApiGroup_RegoResponseVector(t *testing.T) { }) } } + +// TestGetResourceExceptions_LabelDesignatorOnNonWorkloads pins that a label in the +// designator is a constraint on every object type. RegoResponseVector, host-sensor and +// cloud-provider objects have no labels to compare, so a label-scoped exception must +// not match them unless a related object of the vector carries the label. +func TestGetResourceExceptions_LabelDesignatorOnNonWorkloads(t *testing.T) { + labelOnly := armotypes.PostureExceptionPolicy{ + PortalBase: armotypes.PortalBase{Name: "nginx-only"}, + Resources: []identifiers.PortalDesignator{{ + DesignatorType: identifiers.DesignatorAttributes, + Attributes: map[string]string{"app": "nginx"}, + }}, + } + nameOnly := armotypes.PostureExceptionPolicy{ + PortalBase: armotypes.PortalBase{Name: "node-1"}, + Resources: []identifiers.PortalDesignator{{ + DesignatorType: identifiers.DesignatorAttributes, + Attributes: map[string]string{"name": "node-1"}, + }}, + } + + subject := func(bindingLabels map[string]interface{}) workloadinterface.IMetadata { + binding := map[string]interface{}{"name": "alice-admin"} + if bindingLabels != nil { + binding["labels"] = bindingLabels + } + return objectsenvelopes.NewRegoResponseVectorObject(map[string]interface{}{ + "apiGroup": "rbac.authorization.k8s.io", + "kind": "User", + "name": "alice", + "relatedObjects": []interface{}{ + map[string]interface{}{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRoleBinding", + "metadata": binding, + "roleRef": map[string]interface{}{"kind": "ClusterRole", "name": "cluster-admin"}, + }, + map[string]interface{}{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": map[string]interface{}{"name": "cluster-admin"}, + }, + }, + }) + } + hostSensor := objectsenvelopes.NewObject(map[string]interface{}{ + "apiVersion": "hostdata.kubescape.cloud/v1beta0", + "kind": "KubeletInfo", + "metadata": map[string]interface{}{"name": "node-1"}, + "data": map[string]interface{}{}, + }) + + tests := []struct { + name string + policy armotypes.PostureExceptionPolicy + workload workloadinterface.IMetadata + expected int + }{ + {"label-only exception does not match an RBAC subject without the label", labelOnly, subject(nil), 0}, + {"label-only exception matches an RBAC subject whose binding has the label", labelOnly, subject(map[string]interface{}{"app": "nginx"}), 1}, + {"label-only exception does not match a host-sensor object", labelOnly, hostSensor, 0}, + {"name-only exception still matches a host-sensor object", nameOnly, hostSensor, 1}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := NewProcessor().GetResourceExceptions([]armotypes.PostureExceptionPolicy{tt.policy}, tt.workload, "") + assert.Len(t, got, tt.expected) + }) + } +}