• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kubescape / opa-utils / 25802233878

13 May 2026 01:29PM UTC coverage: 68.936% (+0.9%) from 68.078%
25802233878

push

github

web-flow
Merge pull request #169 from Tomgrinds777/fix/container-level-exceptions

feat(exceptions): support container-level exceptions via containerName attribute

92 of 104 new or added lines in 2 files covered. (88.46%)

3271 of 4745 relevant lines covered (68.94%)

120.08 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.65
/exceptions/exceptionprocessor.go
1
package exceptions
2

3
import (
4
        "regexp"
5
        "strconv"
6
        "strings"
7

8
        "github.com/armosec/armoapi-go/identifiers"
9

10
        "github.com/kubescape/k8s-interface/workloadinterface"
11
        "github.com/kubescape/opa-utils/objectsenvelopes"
12
        "github.com/kubescape/opa-utils/reporthandling"
13

14
        "github.com/armosec/armoapi-go/armotypes"
15
)
16

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

21
// Processor processes exceptions.
22
type Processor struct {
23
        *comparator
24
        designatorCache *designatorCache
25
}
26

27
func NewProcessor() *Processor {
10✔
28
        return &Processor{
10✔
29
                comparator:      newComparator(),
10✔
30
                designatorCache: newDesignatorCache(),
10✔
31
        }
10✔
32
}
10✔
33

34
// SetFrameworkExceptions add exceptions to framework report
35
func (p *Processor) SetFrameworkExceptions(frameworkReport *reporthandling.FrameworkReport, exceptionsPolicies []armotypes.PostureExceptionPolicy, clusterName string) {
×
36
        for c := range frameworkReport.ControlReports {
×
37
                p.SetControlExceptions(&frameworkReport.ControlReports[c], exceptionsPolicies, clusterName, frameworkReport.Name)
×
38
        }
×
39
}
40

41
// SetControlExceptions add exceptions to control report
42
func (p *Processor) SetControlExceptions(controlReport *reporthandling.ControlReport, exceptionsPolicies []armotypes.PostureExceptionPolicy, clusterName, frameworkName string) {
×
43
        for r := range controlReport.RuleReports {
×
44
                p.SetRuleExceptions(&controlReport.RuleReports[r], exceptionsPolicies, clusterName, frameworkName, controlReport.ControlID)
×
45
        }
×
46
}
47

48
// SetRuleExceptions add exceptions to rule report
49
func (p *Processor) SetRuleExceptions(ruleReport *reporthandling.RuleReport, exceptionsPolicies []armotypes.PostureExceptionPolicy, clusterName, frameworkName, controlID string) {
×
50
        // adding exceptions to the rules
×
51
        ruleExceptions := p.ListRuleExceptions(exceptionsPolicies, frameworkName, controlID, ruleReport.Name)
×
52
        p.SetRuleResponsExceptions(ruleReport.RuleResponses, ruleExceptions, clusterName)
×
53
}
×
54

55
// SetRuleExceptions add exceptions to rule respons structure
56
func (p *Processor) SetRuleResponsExceptions(results []reporthandling.RuleResponse, ruleExceptions []armotypes.PostureExceptionPolicy, clusterName string) {
8✔
57
        if len(ruleExceptions) == 0 {
8✔
58
                return
×
59
        }
×
60

61
        for i := range results {
16✔
62
                workloads := alertObjectToWorkloads(&results[i].AlertObject)
8✔
63
                if len(workloads) == 0 {
8✔
64
                        continue
×
65
                }
66

67
                for w := range workloads {
16✔
68
                        // Resolve which containers actually produced the finding so that a
8✔
69
                        // containerName exception is only applied when the excepted container
8✔
70
                        // is the one that failed, not just any container in the pod.
8✔
71
                        failingContainerNames := extractFailingContainerNames(results[i].FailedPaths, workloads[w])
8✔
72
                        if exceptions := p.getResourceExceptions(ruleExceptions, workloads[w], clusterName, failingContainerNames); len(exceptions) > 0 {
13✔
73
                                results[i].Exception = &exceptions[0]
5✔
74
                        }
5✔
75
                }
76

77
                results[i].RuleStatus = results[i].GetStatus()
8✔
78
        }
79
}
80

81
func (p *Processor) ListRuleExceptions(exceptionPolicies []armotypes.PostureExceptionPolicy, frameworkName, controlID, ruleName string) []armotypes.PostureExceptionPolicy {
11✔
82
        ruleExceptions := make([]armotypes.PostureExceptionPolicy, 0, len(exceptionPolicies))
11✔
83

11✔
84
        for i := range exceptionPolicies {
22✔
85
                if p.ruleHasExceptions(&exceptionPolicies[i], frameworkName, controlID, ruleName) {
19✔
86
                        ruleExceptions = append(ruleExceptions, exceptionPolicies[i])
8✔
87
                }
8✔
88
        }
89

90
        return ruleExceptions[:len(ruleExceptions):len(ruleExceptions)]
11✔
91

92
}
93

94
func (p *Processor) ruleHasExceptions(exceptionPolicy *armotypes.PostureExceptionPolicy, frameworkName, controlID, ruleName string) bool {
11✔
95
        if len(exceptionPolicy.PosturePolicies) == 0 {
12✔
96
                return true // empty policy -> apply all
1✔
97
        }
1✔
98

99
        for _, posturePolicy := range exceptionPolicy.PosturePolicies {
20✔
100
                if posturePolicy.FrameworkName == "" && posturePolicy.ControlID == "" && posturePolicy.RuleName == "" {
10✔
101
                        return true // empty policy -> apply all
×
102
                }
×
103
                if posturePolicy.FrameworkName != "" && frameworkName != "" && !(strings.EqualFold(posturePolicy.FrameworkName, frameworkName) || p.regexCompareI(posturePolicy.FrameworkName, frameworkName)) {
12✔
104
                        continue // policy does not match
2✔
105
                }
106
                if posturePolicy.ControlID != "" && controlID != "" && !(strings.EqualFold(posturePolicy.ControlID, controlID) || p.regexCompareI(posturePolicy.ControlID, controlID)) {
8✔
107
                        continue // policy does not match
×
108
                }
109
                if posturePolicy.RuleName != "" && ruleName != "" && !(strings.EqualFold(posturePolicy.RuleName, ruleName) || p.regexCompareI(posturePolicy.RuleName, ruleName)) {
9✔
110
                        continue // policy does not match
1✔
111
                }
112

113
                return true // policies match
7✔
114
        }
115

116
        return false
3✔
117

118
}
119

120
func alertObjectToWorkloads(obj *reporthandling.AlertObject) []workloadinterface.IMetadata {
8✔
121
        resources := make([]workloadinterface.IMetadata, 0, len(obj.K8SApiObjects)+1)
8✔
122

8✔
123
        for i := range obj.K8SApiObjects {
14✔
124
                r := objectsenvelopes.NewObject(obj.K8SApiObjects[i])
6✔
125
                if r == nil {
6✔
126
                        continue
×
127
                }
128

129
                resources = append(resources, r)
6✔
130
                /*
131
                        ns : = r.GetNamespace()
132
                        if ns != "" {
133
                                // TODO - handle empty namespace
134
                        }
135
                */
136
        }
137

138
        if obj.ExternalObjects != nil {
10✔
139
                if r := objectsenvelopes.NewObject(obj.ExternalObjects); r != nil {
4✔
140
                        // TODO - What about linked objects?
2✔
141
                        resources = append(resources, r)
2✔
142
                }
2✔
143
        }
144

145
        return resources[:len(resources):len(resources)]
8✔
146
}
147

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.
151
func (p *Processor) GetResourceExceptions(ruleExceptions []armotypes.PostureExceptionPolicy, workload workloadinterface.IMetadata, clusterName string) []armotypes.PostureExceptionPolicy {
9✔
152
        return p.getResourceExceptions(ruleExceptions, workload, clusterName, nil)
9✔
153
}
9✔
154

155
func (p *Processor) getResourceExceptions(ruleExceptions []armotypes.PostureExceptionPolicy, workload workloadinterface.IMetadata, clusterName string, failingContainerNames []string) []armotypes.PostureExceptionPolicy {
17✔
156
        // no pre-allocation since most of the time it's empty or has only one element
17✔
157
        var postureExceptionPolicy []armotypes.PostureExceptionPolicy
17✔
158

17✔
159
        for _, ruleException := range ruleExceptions {
34✔
160
                for _, resourceToPin := range ruleException.Resources {
34✔
161
                        resource := resourceToPin
17✔
162
                        if p.hasException(clusterName, &resource, workload, failingContainerNames) {
28✔
163
                                postureExceptionPolicy = append(postureExceptionPolicy, ruleException)
11✔
164
                        }
11✔
165
                }
166
        }
167

168
        return postureExceptionPolicy
17✔
169
}
170

171
// RegexCompareControlID reports whether pattern case-insensitively matches target.
172
func (p *Processor) RegexCompareControlID(pattern, target string) bool {
×
173
        return p.regexCompareI(pattern, target)
×
174
}
×
175

176
// MatchesCluster reports whether the designator's cluster constraint matches clusterName.
177
// A nil designator or empty cluster field matches any cluster.
178
func (p *Processor) MatchesCluster(designator *identifiers.PortalDesignator, clusterName string) bool {
×
179
        if designator == nil {
×
180
                return true
×
181
        }
×
182
        return p.matchesCluster(p.getAttributes(designator), clusterName)
×
183
}
184

185
// getAttributes returns digested attributes, using the cache when available.
186
func (p *Processor) getAttributes(designator *identifiers.PortalDesignator) identifiers.AttributesDesignators {
49✔
187
        if attrs, ok := p.designatorCache.Get(designator); ok {
68✔
188
                return attrs
19✔
189
        }
19✔
190
        attrs := designator.DigestPortalDesignator()
30✔
191
        p.designatorCache.Set(designator, attrs)
30✔
192
        return attrs
30✔
193
}
194

195
// matchesCluster checks the cluster constraint against pre-digested attributes.
196
func (p *Processor) matchesCluster(attributes identifiers.AttributesDesignators, clusterName string) bool {
48✔
197
        cluster := attributes.GetCluster()
48✔
198
        if cluster == "" {
90✔
199
                return true
42✔
200
        }
42✔
201
        return p.compareCluster(cluster, clusterName)
6✔
202
}
203

204
func (p *Processor) hasException(clusterName string, designator *identifiers.PortalDesignator, workload workloadinterface.IMetadata, failingContainerNames []string) bool {
49✔
205
        attributes := p.getAttributes(designator)
49✔
206

49✔
207
        if attributes.GetCluster() == "" && attributes.GetNamespace() == "" && attributes.GetKind() == "" && attributes.GetName() == "" && attributes.GetResourceID() == "" && attributes.GetPath() == "" && len(attributes.GetLabels()) == 0 {
50✔
208
                return false // if designators are empty
1✔
209
        }
1✔
210

211
        if !p.matchesCluster(attributes, clusterName) {
49✔
212
                return false // cluster name does not match
1✔
213
        }
1✔
214

215
        if isTypeRegoResponseVector(workload) {
58✔
216
                if p.iterateRegoResponseVector(workload, attributes, failingContainerNames) {
15✔
217
                        return true
4✔
218
                }
4✔
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 {
10✔
224
                        return false
3✔
225
                }
3✔
226
                // otherwise, continue to check the base object
227
        }
228
        return p.metadataHasException(workload, attributes, failingContainerNames)
40✔
229

230
}
231

232
func (p *Processor) metadataHasException(workload workloadinterface.IMetadata, attributes identifiers.AttributesDesignators, failingContainerNames []string) bool {
71✔
233

71✔
234
        if attributes.GetNamespace() != "" && !p.compareNamespace(workload, attributes.GetNamespace()) {
73✔
235
                return false // namespaces do not match
2✔
236
        }
2✔
237

238
        if attributes.GetKind() != "" && !p.compareKind(workload, attributes.GetKind()) {
75✔
239
                return false // kinds do not match
6✔
240
        }
6✔
241

242
        if attributes.GetName() != "" && !p.compareName(workload, attributes.GetName()) {
69✔
243
                return false // names do not match
6✔
244
        }
6✔
245

246
        if attributes.GetResourceID() != "" && !p.compareResourceID(workload, attributes.GetResourceID()) {
58✔
247
                return false // names do not match
1✔
248
        }
1✔
249

250
        if attributes.GetPath() != "" && !p.comparePath(workload, attributes.GetPath()) {
57✔
251
                return false // paths do not match
1✔
252
        }
1✔
253

254
        if isTypeWorkload(workload) {
100✔
255
                allLabels := attributes.GetLabels()
45✔
256
                containerName, hasContainerName := allLabels[identifiers.AttributeContainerName]
45✔
257

45✔
258
                // Build a label map with containerName stripped out so it is not
45✔
259
                // treated as a Kubernetes label during label/annotation comparison.
45✔
260
                labelsWithoutContainer := allLabels
45✔
261
                if hasContainerName {
59✔
262
                        labelsWithoutContainer = make(map[string]string, len(allLabels)-1)
14✔
263
                        for k, v := range allLabels {
28✔
264
                                if k != identifiers.AttributeContainerName {
14✔
NEW
265
                                        labelsWithoutContainer[k] = v
×
NEW
266
                                }
×
267
                        }
268
                }
269

270
                if len(labelsWithoutContainer) > 0 {
72✔
271
                        if !p.compareLabels(workload, labelsWithoutContainer) && !p.compareAnnotations(workload, labelsWithoutContainer) {
41✔
272
                                return false // labels nor annotations do not match
14✔
273
                        }
14✔
274
                }
275

276
                if hasContainerName && !p.compareContainerName(workload, containerName, failingContainerNames) {
36✔
277
                        return false // container name does not match
5✔
278
                }
5✔
279
        }
280

281
        return true
36✔
282
}
283

284
func (p *Processor) iterateRegoResponseVector(workload workloadinterface.IMetadata, attributes identifiers.AttributesDesignators, failingContainerNames []string) bool {
17✔
285
        v := objectsenvelopes.NewRegoResponseVectorObject(workload.GetObject())
17✔
286
        for _, r := range v.GetRelatedObjects() {
42✔
287
                if p.metadataHasException(r, attributes, failingContainerNames) {
33✔
288
                        return true
8✔
289
                }
8✔
290
        }
291
        return false
9✔
292
}
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 {
12✔
305
        if len(paths) == 0 {
14✔
306
                return nil
2✔
307
        }
2✔
308

309
        if isTypeRegoResponseVector(workload) {
14✔
310
                v := objectsenvelopes.NewRegoResponseVectorObject(workload.GetObject())
4✔
311
                seen := make(map[string]struct{})
4✔
312
                for _, r := range v.GetRelatedObjects() {
8✔
313
                        for _, name := range extractFailingContainerNames(paths, r) {
8✔
314
                                seen[name] = struct{}{}
4✔
315
                        }
4✔
316
                }
317
                if len(seen) == 0 {
4✔
NEW
318
                        return nil
×
NEW
319
                }
×
320
                names := make([]string, 0, len(seen))
4✔
321
                for name := range seen {
8✔
322
                        names = append(names, name)
4✔
323
                }
4✔
324
                return names
4✔
325
        }
326

327
        wl := workloadinterface.NewWorkloadObj(workload.GetObject())
6✔
328
        containers, _ := wl.GetContainers()
6✔
329
        initContainers, _ := wl.GetInitContainers()
6✔
330
        if len(containers)+len(initContainers) == 0 {
6✔
NEW
331
                return nil
×
NEW
332
        }
×
333

334
        seen := make(map[string]struct{})
6✔
335
        for _, path := range paths {
12✔
336
                for _, m := range rexContainerPath.FindAllStringSubmatch(path, -1) {
12✔
337
                        idx, err := strconv.Atoi(m[2])
6✔
338
                        if err != nil {
6✔
NEW
339
                                continue
×
340
                        }
341
                        if m[1] == "initC" {
6✔
NEW
342
                                if idx < len(initContainers) {
×
NEW
343
                                        seen[initContainers[idx].Name] = struct{}{}
×
NEW
344
                                }
×
345
                        } else {
6✔
346
                                if idx < len(containers) {
12✔
347
                                        seen[containers[idx].Name] = struct{}{}
6✔
348
                                }
6✔
349
                        }
350
                }
351
        }
352

353
        if len(seen) == 0 {
6✔
NEW
354
                return nil
×
NEW
355
        }
×
356
        names := make([]string, 0, len(seen))
6✔
357
        for name := range seen {
12✔
358
                names = append(names, name)
6✔
359
        }
6✔
360
        return names
6✔
361
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc