-
Notifications
You must be signed in to change notification settings - Fork 8
/
report.go
383 lines (349 loc) · 12.7 KB
/
report.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
package report
import (
"encoding/json"
"fmt"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/wg-policy-prototypes/policy-report/pkg/api/wgpolicyk8s.io/v1alpha2"
"github.com/kubewarden/audit-scanner/internal/constants"
policiesv1 "github.com/kubewarden/kubewarden-controller/pkg/apis/policies/v1"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
admv1 "k8s.io/api/admission/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
)
type PolicyReport struct {
v1alpha2.PolicyReport
}
type ClusterPolicyReport struct {
v1alpha2.ClusterPolicyReport
}
const (
// Status specifies state of a policy result
StatusPass = "pass"
StatusFail = "fail"
StatusWarn = "warn"
StatusError = "error"
StatusSkip = "skip"
// Severity specifies severity of a policy result
SeverityCritical = "critical"
SeverityHigh = "high"
SeverityMedium = "medium"
SeverityLow = "low"
SeverityInfo = "info"
// Category specifies the category of a policy result
TypeMutating = "mutating"
TypeValidating = "validating"
TypeContextAware = "context-aware"
ValueTypeTrue = "true"
LabelAppManagedBy = "app.kubernetes.io/managed-by"
LabelApp = "kubewarden"
)
func NewClusterPolicyReport(name string) ClusterPolicyReport {
labels := map[string]string{}
labels[LabelAppManagedBy] = LabelApp
return ClusterPolicyReport{
ClusterPolicyReport: v1alpha2.ClusterPolicyReport{
ObjectMeta: metav1.ObjectMeta{
Name: getClusterReportName(name),
CreationTimestamp: metav1.Now(),
Labels: labels,
},
Summary: v1alpha2.PolicyReportSummary{
Pass: 0, // count of policies with requirements met
Fail: 0, // count of policies with requirements not met
Warn: 0, // not used for now
Error: 0, // count of policies that couldn't be evaluated
Skip: 0, // count of policies that were not selected for evaluation
},
Results: []*v1alpha2.PolicyReportResult{},
},
}
}
func NewPolicyReport(namespace *v1.Namespace) PolicyReport {
labels := map[string]string{}
labels[LabelAppManagedBy] = LabelApp
return PolicyReport{
PolicyReport: v1alpha2.PolicyReport{
ObjectMeta: metav1.ObjectMeta{
Name: getNamespacedReportName(namespace.Name),
Namespace: namespace.Name,
CreationTimestamp: metav1.Now(),
Labels: labels,
},
Scope: &v1.ObjectReference{
Kind: namespace.Kind,
Namespace: "",
Name: namespace.Name,
UID: namespace.UID,
APIVersion: namespace.APIVersion,
ResourceVersion: namespace.ResourceVersion,
},
Summary: v1alpha2.PolicyReportSummary{
Pass: 0, // count of policies with requirements met
Fail: 0, // count of policies with requirements not met
Warn: 0, // not used for now
Error: 0, // count of policies that couldn't be evaluated
Skip: 0, // count of policies that were not selected for evaluation
},
Results: []*v1alpha2.PolicyReportResult{},
},
}
}
func getClusterReportName(name string) string {
return PrefixNameClusterPolicyReport + name
}
func getNamespacedReportName(namespace string) string {
return PrefixNamePolicyReport + namespace
}
func (r *PolicyReport) AddResult(result *v1alpha2.PolicyReportResult) {
switch result.Result {
case StatusFail:
r.Summary.Fail++
case StatusError:
r.Summary.Error++
case StatusPass:
r.Summary.Pass++
}
r.Results = append(r.Results, result)
}
// GetSummaryJSON gets the report.Summary formatted in JSON. Useful for logging
func (r *PolicyReport) GetSummaryJSON() (string, error) {
marshaled, err := json.Marshal(r.Summary)
if err != nil {
return "error marshalling summary", err
}
return string(marshaled), nil
}
// GetSummaryJSON gets the report.Summary formatted in JSON. Useful for logging
func (r *ClusterPolicyReport) GetSummaryJSON() (string, error) {
marshaled, err := json.Marshal(r.Summary)
if err != nil {
return "error marshalling summary", err
}
return string(marshaled), nil
}
// GetReusablePolicyReportResult tries to find a PolicyReportResult that
// can be reused.
//
// The result can be reused if both these conditions are
// satisfied:
// - The subject of the PolicyReportResult (the object that was inspected)
// has not been changed since the report was created
// - The policy that evaluated the subject (now given by the user as
// parameter) has not been changed since the report was created
func (r *PolicyReport) GetReusablePolicyReportResult(policy policiesv1.Policy, resource unstructured.Unstructured) *v1alpha2.PolicyReportResult {
return findReusableResult(r.Results, policy, resource)
}
func (r *PolicyReport) CreateResult(
policy policiesv1.Policy, resource unstructured.Unstructured,
auditResponse *admv1.AdmissionReview, responseErr error,
) *v1alpha2.PolicyReportResult {
result := newPolicyReportResult(policy, resource, auditResponse, responseErr)
log.Debug().
Str("report name", r.Name).
Dict("result", zerolog.Dict().
Str("policy", policy.GetName()).
Str("resource", resource.GetName()).
Bool("allowed", auditResponse.Response.Allowed).
Str("result", string(result.Result)),
).Msg("added result to report")
return result
}
func (r *ClusterPolicyReport) CreateResult(
policy policiesv1.Policy, resource unstructured.Unstructured,
auditResponse *admv1.AdmissionReview, responseErr error,
) *v1alpha2.PolicyReportResult {
result := newPolicyReportResult(policy, resource, auditResponse, responseErr)
log.Debug().Str("report name", r.Name).Dict("result", zerolog.Dict().
Str("policy", policy.GetName()).Str("resource", resource.GetName()).
Bool("allowed", auditResponse.Response.Allowed).
Str("result", string(result.Result)),
).Msg("added result to report")
return result
}
func (r *ClusterPolicyReport) AddResult(result *v1alpha2.PolicyReportResult) {
switch result.Result {
case StatusFail:
r.Summary.Fail++
case StatusError:
r.Summary.Error++
case StatusPass:
r.Summary.Pass++
}
r.Results = append(r.Results, result)
}
// GetReusablePolicyReportResult tries to find a PolicyReportResult that
// can be reused.
//
// The result can be reused if both these conditions are
// satisfied:
// - The subject of the PolicyReportResult (the object that was inspected)
// has not been changed since the report was created
// - The policy that evaluated the subject (now given by the user as
// parameter) has not been changed since the report was created
func (r *ClusterPolicyReport) GetReusablePolicyReportResult(policy policiesv1.Policy, resource unstructured.Unstructured) *v1alpha2.PolicyReportResult {
return findReusableResult(r.Results, policy, resource)
}
// isReportGeneratedByPolicy checks if the given PolicyReportResult
// has been generated by the given policy.
// The comparison uses the policy UID and its revision and checks them
// with the metadata stored inside of the given PolicyReportResult
func isReportGeneratedByPolicy(result *v1alpha2.PolicyReportResult, policy policiesv1.Policy) (bool, error) {
policyName, err := getPolicyName(policy)
if err != nil {
return false, err
}
policyResourceVersion, hasPolicyResourceVersion := result.Properties[PropertyPolicyResourceVersion]
policyUID, hasPolicyUID := result.Properties[PropertyPolicyUID]
return result.Policy == policyName &&
hasPolicyResourceVersion && policyResourceVersion == policy.GetResourceVersion() &&
hasPolicyUID && types.UID(policyUID) == policy.GetUID(), nil
}
// findReusableResult returns the PolicyReportResult that refers to the same policy and
// resource from the given parameters.
// A policy is considered the same if it has the same UID and resourceVersion.
// Check more in the isReportGeneratedByPolicy function
// A resource is considered the same if its resource object reference matches with some resource from the report
func findReusableResult(results []*v1alpha2.PolicyReportResult, policy policiesv1.Policy, resource unstructured.Unstructured) *v1alpha2.PolicyReportResult {
resourceObjReference := getResourceObjectReference(resource)
for _, result := range results {
isSamePolicy, err := isReportGeneratedByPolicy(result, policy)
if err != nil {
log.Error().Err(err).
Dict("policy", zerolog.Dict().
Str("resultPolicy", result.Policy).
Str("uid", string(policy.GetUID())).
Str("name", policy.GetName())).
Msg("cannot check if PolicyReportResult has been generated by the given policy")
continue
}
if isSamePolicy {
for _, objReference := range result.Subjects {
if resourceObjReference == *objReference {
return result
}
}
}
}
return nil
}
func getPolicyName(policy policiesv1.Policy) (string, error) {
switch policy.GetObjectKind().GroupVersionKind() {
case schema.GroupVersionKind{
Group: constants.KubewardenPoliciesGroup,
Version: constants.KubewardenPoliciesVersion,
Kind: constants.KubewardenKindClusterAdmissionPolicy,
}:
return "cap-" + policy.GetName(), nil
case schema.GroupVersionKind{
Group: constants.KubewardenPoliciesGroup,
Version: constants.KubewardenPoliciesVersion,
Kind: constants.KubewardenKindAdmissionPolicy,
}:
return "ap-" + policy.GetName(), nil
default:
// this should never happens
log.Fatal().Msg("cannot generate policy name")
return "", fmt.Errorf("cannot generate policy name")
}
}
func getResourceObjectReference(resource unstructured.Unstructured) v1.ObjectReference {
return v1.ObjectReference{
Kind: resource.GetKind(),
Namespace: resource.GetNamespace(),
Name: resource.GetName(),
UID: resource.GetUID(),
APIVersion: resource.GetAPIVersion(),
ResourceVersion: resource.GetResourceVersion(),
}
}
//nolint:funlen
func newPolicyReportResult(
policy policiesv1.Policy, resource unstructured.Unstructured,
auditResponse *admv1.AdmissionReview, responseErr error,
) *v1alpha2.PolicyReportResult {
var result v1alpha2.PolicyResult
var description string
if responseErr != nil {
result = StatusError
description = auditResponse.Response.Result.Message
} else {
if auditResponse.Response.Allowed {
result = StatusPass
} else {
result = StatusFail
description = auditResponse.Response.Result.Message
}
}
name, _ := getPolicyName(policy)
time := metav1.Now()
timestamp := *time.ProtoTime()
var severity v1alpha2.PolicyResultSeverity
var scored bool
if policy.GetPolicyMode() == policiesv1.PolicyMode(policiesv1.PolicyModeStatusMonitor) {
scored = true
severity = SeverityInfo
} else {
if sev, present := policy.GetSeverity(); present {
scored = true
switch sev {
case SeverityCritical:
severity = SeverityCritical
case SeverityHigh:
severity = SeverityHigh
case SeverityMedium:
severity = SeverityMedium
case SeverityLow:
severity = SeverityLow
default:
// this should never happen
log.Error().
Dict("result", zerolog.Dict().
Str("policy", policy.GetName()).
Str("resource", resource.GetName()).
Bool("allowed", auditResponse.Response.Allowed).
Str("severity", sev),
).Msg("severity unknown")
}
}
}
var category string
if cat, present := policy.GetCategory(); present {
category = cat
}
properties := map[string]string{}
if policy.IsMutating() {
properties[TypeMutating] = ValueTypeTrue
} else {
properties[TypeValidating] = ValueTypeTrue
}
if policy.IsContextAware() {
properties[TypeContextAware] = ValueTypeTrue
}
// The policy resource version and the policy UID are used to check if the
// same result can be reused in the next scan
// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency
properties[PropertyPolicyResourceVersion] = policy.GetResourceVersion()
properties[PropertyPolicyUID] = string(policy.GetUID())
rule := policy.GetName()
resourceObjectReference := getResourceObjectReference(resource)
return &v1alpha2.PolicyReportResult{
Source: PolicyReportSource,
Policy: name, // either cap-policy_name or ap-policy_name
Rule: rule, // policy name
Category: category, // either validating, or mutating and validating
Severity: severity, // either info for monitor or empty
// Timestamp shouldn't be used in go structs, and only gives seconds
// https://github.com/kubernetes/apimachinery/blob/v0.27.2/pkg/apis/meta/v1/time_proto.go#LL48C9-L48C9
Timestamp: timestamp, // time the result was computed
Result: result, // pass, fail, error
Scored: scored,
Subjects: []*v1.ObjectReference{&resourceObjectReference}, // reference to object evaluated
SubjectSelector: &metav1.LabelSelector{},
Description: description, // output message of the policy
Properties: properties,
}
}