11package exceptions
22
33import (
4+ "regexp"
5+ "strconv"
46 "strings"
57
68 "github.com/armosec/armoapi-go/identifiers"
@@ -12,6 +14,10 @@ import (
1214 "github.com/armosec/armoapi-go/armotypes"
1315)
1416
17+ // rexContainerPath matches "containers[N]" and "initContainers[N]" in a
18+ // FailedPath so we can resolve the container index to a name.
19+ var rexContainerPath = regexp .MustCompile (`(initC|c)ontainers\[(\d+)\]` )
20+
1521// Processor processes exceptions.
1622type Processor struct {
1723 * comparator
@@ -59,7 +65,11 @@ func (p *Processor) SetRuleResponsExceptions(results []reporthandling.RuleRespon
5965 }
6066
6167 for w := range workloads {
62- if exceptions := p .GetResourceExceptions (ruleExceptions , workloads [w ], clusterName ); len (exceptions ) > 0 {
68+ // Resolve which containers actually produced the finding so that a
69+ // containerName exception is only applied when the excepted container
70+ // is the one that failed, not just any container in the pod.
71+ failingContainerNames := extractFailingContainerNames (results [i ].FailedPaths , workloads [w ])
72+ if exceptions := p .getResourceExceptions (ruleExceptions , workloads [w ], clusterName , failingContainerNames ); len (exceptions ) > 0 {
6373 results [i ].Exception = & exceptions [0 ]
6474 }
6575 }
@@ -135,15 +145,21 @@ func alertObjectToWorkloads(obj *reporthandling.AlertObject) []workloadinterface
135145 return resources [:len (resources ):len (resources )]
136146}
137147
138- // GetResourceException get exceptions of single resource
148+ // GetResourceExceptions returns the exception policies that match workload.
149+ // It checks container membership across the whole workload; use
150+ // SetRuleResponsExceptions when FailedPaths are available for precise matching.
139151func (p * Processor ) GetResourceExceptions (ruleExceptions []armotypes.PostureExceptionPolicy , workload workloadinterface.IMetadata , clusterName string ) []armotypes.PostureExceptionPolicy {
152+ return p .getResourceExceptions (ruleExceptions , workload , clusterName , nil )
153+ }
154+
155+ func (p * Processor ) getResourceExceptions (ruleExceptions []armotypes.PostureExceptionPolicy , workload workloadinterface.IMetadata , clusterName string , failingContainerNames []string ) []armotypes.PostureExceptionPolicy {
140156 // no pre-allocation since most of the time it's empty or has only one element
141157 var postureExceptionPolicy []armotypes.PostureExceptionPolicy
142158
143159 for _ , ruleException := range ruleExceptions {
144160 for _ , resourceToPin := range ruleException .Resources {
145161 resource := resourceToPin
146- if p .hasException (clusterName , & resource , workload ) {
162+ if p .hasException (clusterName , & resource , workload , failingContainerNames ) {
147163 postureExceptionPolicy = append (postureExceptionPolicy , ruleException )
148164 }
149165 }
@@ -185,8 +201,7 @@ func (p *Processor) matchesCluster(attributes identifiers.AttributesDesignators,
185201 return p .compareCluster (cluster , clusterName )
186202}
187203
188- // compareMetadata - compare namespace and kind
189- func (p * Processor ) hasException (clusterName string , designator * identifiers.PortalDesignator , workload workloadinterface.IMetadata ) bool {
204+ func (p * Processor ) hasException (clusterName string , designator * identifiers.PortalDesignator , workload workloadinterface.IMetadata , failingContainerNames []string ) bool {
190205 attributes := p .getAttributes (designator )
191206
192207 if attributes .GetCluster () == "" && attributes .GetNamespace () == "" && attributes .GetKind () == "" && attributes .GetName () == "" && attributes .GetResourceID () == "" && attributes .GetPath () == "" && len (attributes .GetLabels ()) == 0 {
@@ -198,16 +213,23 @@ func (p *Processor) hasException(clusterName string, designator *identifiers.Por
198213 }
199214
200215 if isTypeRegoResponseVector (workload ) {
201- if p .iterateRegoResponseVector (workload , attributes ) {
216+ if p .iterateRegoResponseVector (workload , attributes , failingContainerNames ) {
202217 return true
203218 }
219+ // If containerName is in the designator, stop here: the base
220+ // RegoResponseVector object is not a workload, so container membership
221+ // cannot be verified on it. Falling through would silently skip the
222+ // container check and produce false positives.
223+ if _ , ok := attributes .GetLabels ()[identifiers .AttributeContainerName ]; ok {
224+ return false
225+ }
204226 // otherwise, continue to check the base object
205227 }
206- return p .metadataHasException (workload , attributes )
228+ return p .metadataHasException (workload , attributes , failingContainerNames )
207229
208230}
209231
210- func (p * Processor ) metadataHasException (workload workloadinterface.IMetadata , attributes identifiers.AttributesDesignators ) bool {
232+ func (p * Processor ) metadataHasException (workload workloadinterface.IMetadata , attributes identifiers.AttributesDesignators , failingContainerNames [] string ) bool {
211233
212234 if attributes .GetNamespace () != "" && ! p .compareNamespace (workload , attributes .GetNamespace ()) {
213235 return false // namespaces do not match
@@ -229,20 +251,111 @@ func (p *Processor) metadataHasException(workload workloadinterface.IMetadata, a
229251 return false // paths do not match
230252 }
231253
232- if isTypeWorkload (workload ) && len (attributes .GetLabels ()) > 0 {
233- if ! p .compareLabels (workload , attributes .GetLabels ()) && ! p .compareAnnotations (workload , attributes .GetLabels ()) {
234- return false // labels nor annotations do not match
254+ if isTypeWorkload (workload ) {
255+ allLabels := attributes .GetLabels ()
256+ containerName , hasContainerName := allLabels [identifiers .AttributeContainerName ]
257+
258+ // Build a label map with containerName stripped out so it is not
259+ // treated as a Kubernetes label during label/annotation comparison.
260+ labelsWithoutContainer := allLabels
261+ if hasContainerName {
262+ labelsWithoutContainer = make (map [string ]string , len (allLabels )- 1 )
263+ for k , v := range allLabels {
264+ if k != identifiers .AttributeContainerName {
265+ labelsWithoutContainer [k ] = v
266+ }
267+ }
268+ }
269+
270+ if len (labelsWithoutContainer ) > 0 {
271+ if ! p .compareLabels (workload , labelsWithoutContainer ) && ! p .compareAnnotations (workload , labelsWithoutContainer ) {
272+ return false // labels nor annotations do not match
273+ }
274+ }
275+
276+ if hasContainerName && ! p .compareContainerName (workload , containerName , failingContainerNames ) {
277+ return false // container name does not match
235278 }
236279 }
280+
237281 return true
238282}
239283
240- func (p * Processor ) iterateRegoResponseVector (workload workloadinterface.IMetadata , attributes identifiers.AttributesDesignators ) bool {
284+ func (p * Processor ) iterateRegoResponseVector (workload workloadinterface.IMetadata , attributes identifiers.AttributesDesignators , failingContainerNames [] string ) bool {
241285 v := objectsenvelopes .NewRegoResponseVectorObject (workload .GetObject ())
242286 for _ , r := range v .GetRelatedObjects () {
243- if p .metadataHasException (r , attributes ) {
287+ if p .metadataHasException (r , attributes , failingContainerNames ) {
244288 return true
245289 }
246290 }
247291 return false
248292}
293+
294+ // extractFailingContainerNames parses paths like "spec.containers[0].…" or
295+ // "spec.template.spec.initContainers[1].…" to find which containers produced
296+ // the finding, then returns their names from the workload spec. When the
297+ // FailedPaths contain no container indices (e.g. pod-level findings) the
298+ // returned slice is nil and compareContainerName falls back to checking all
299+ // containers in the workload.
300+ //
301+ // For RegoResponseVector objects the vector itself carries no containers; the
302+ // containers live in the related objects. We recurse into each related workload
303+ // so that container-index resolution still works for vector-based findings.
304+ func extractFailingContainerNames (paths []string , workload workloadinterface.IMetadata ) []string {
305+ if len (paths ) == 0 {
306+ return nil
307+ }
308+
309+ if isTypeRegoResponseVector (workload ) {
310+ v := objectsenvelopes .NewRegoResponseVectorObject (workload .GetObject ())
311+ seen := make (map [string ]struct {})
312+ for _ , r := range v .GetRelatedObjects () {
313+ for _ , name := range extractFailingContainerNames (paths , r ) {
314+ seen [name ] = struct {}{}
315+ }
316+ }
317+ if len (seen ) == 0 {
318+ return nil
319+ }
320+ names := make ([]string , 0 , len (seen ))
321+ for name := range seen {
322+ names = append (names , name )
323+ }
324+ return names
325+ }
326+
327+ wl := workloadinterface .NewWorkloadObj (workload .GetObject ())
328+ containers , _ := wl .GetContainers ()
329+ initContainers , _ := wl .GetInitContainers ()
330+ if len (containers )+ len (initContainers ) == 0 {
331+ return nil
332+ }
333+
334+ seen := make (map [string ]struct {})
335+ for _ , path := range paths {
336+ for _ , m := range rexContainerPath .FindAllStringSubmatch (path , - 1 ) {
337+ idx , err := strconv .Atoi (m [2 ])
338+ if err != nil {
339+ continue
340+ }
341+ if m [1 ] == "initC" {
342+ if idx < len (initContainers ) {
343+ seen [initContainers [idx ].Name ] = struct {}{}
344+ }
345+ } else {
346+ if idx < len (containers ) {
347+ seen [containers [idx ].Name ] = struct {}{}
348+ }
349+ }
350+ }
351+ }
352+
353+ if len (seen ) == 0 {
354+ return nil
355+ }
356+ names := make ([]string , 0 , len (seen ))
357+ for name := range seen {
358+ names = append (names , name )
359+ }
360+ return names
361+ }
0 commit comments