k8s.io/kubernetes@v1.29.3/test/utils/audit.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package utils 18 19 import ( 20 "bufio" 21 "fmt" 22 "io" 23 "reflect" 24 "sort" 25 "strings" 26 27 "k8s.io/apimachinery/pkg/runtime" 28 "k8s.io/apimachinery/pkg/runtime/schema" 29 "k8s.io/apimachinery/pkg/types" 30 "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating" 31 auditinternal "k8s.io/apiserver/pkg/apis/audit" 32 "k8s.io/apiserver/pkg/audit" 33 ) 34 35 // AuditEvent is a simplified representation of an audit event for testing purposes 36 type AuditEvent struct { 37 ID types.UID 38 Level auditinternal.Level 39 Stage auditinternal.Stage 40 RequestURI string 41 Verb string 42 Code int32 43 User string 44 ImpersonatedUser string 45 ImpersonatedGroups string 46 Resource string 47 Namespace string 48 RequestObject bool 49 ResponseObject bool 50 AuthorizeDecision string 51 52 // The Check functions in this package takes ownerships of these maps. You should 53 // not reference these maps after calling the Check functions. 54 AdmissionWebhookMutationAnnotations map[string]string 55 AdmissionWebhookPatchAnnotations map[string]string 56 57 // Only populated when a filter is provided to testEventFromInternalFiltered 58 CustomAuditAnnotations map[string]string 59 } 60 61 type AuditAnnotationsFilter func(key, val string) bool 62 63 // MissingEventsReport provides an analysis if any events are missing 64 type MissingEventsReport struct { 65 FirstEventChecked *auditinternal.Event 66 LastEventChecked *auditinternal.Event 67 NumEventsChecked int 68 MissingEvents []AuditEvent 69 } 70 71 // String returns a human readable string representation of the report 72 func (m *MissingEventsReport) String() string { 73 return fmt.Sprintf(`missing %d events 74 75 - first event checked: %#v 76 77 - last event checked: %#v 78 79 - number of events checked: %d 80 81 - missing events: %#v`, len(m.MissingEvents), m.FirstEventChecked, m.LastEventChecked, m.NumEventsChecked, m.MissingEvents) 82 } 83 84 // CheckAuditLines searches the audit log for the expected audit lines. 85 func CheckAuditLines(stream io.Reader, expected []AuditEvent, version schema.GroupVersion) (missingReport *MissingEventsReport, err error) { 86 return CheckAuditLinesFiltered(stream, expected, version, nil) 87 } 88 89 // CheckAuditLinesFiltered searches the audit log for the expected audit lines, customAnnotationsFilter 90 // controls which audit annotations are added to AuditEvent.CustomAuditAnnotations. 91 // If the customAnnotationsFilter is nil, AuditEvent.CustomAuditAnnotations will be empty. 92 func CheckAuditLinesFiltered(stream io.Reader, expected []AuditEvent, version schema.GroupVersion, customAnnotationsFilter AuditAnnotationsFilter) (missingReport *MissingEventsReport, err error) { 93 expectations := newAuditEventTracker(expected) 94 95 scanner := bufio.NewScanner(stream) 96 97 missingReport = &MissingEventsReport{ 98 MissingEvents: expected, 99 } 100 101 var i int 102 for i = 0; scanner.Scan(); i++ { 103 line := scanner.Text() 104 105 e := &auditinternal.Event{} 106 decoder := audit.Codecs.UniversalDecoder(version) 107 if err := runtime.DecodeInto(decoder, []byte(line), e); err != nil { 108 return missingReport, fmt.Errorf("failed decoding buf: %s, apiVersion: %s", line, version) 109 } 110 if i == 0 { 111 missingReport.FirstEventChecked = e 112 } 113 missingReport.LastEventChecked = e 114 115 event, err := testEventFromInternalFiltered(e, customAnnotationsFilter) 116 if err != nil { 117 return missingReport, err 118 } 119 120 expectations.Mark(event) 121 } 122 if err := scanner.Err(); err != nil { 123 return missingReport, err 124 } 125 126 missingReport.MissingEvents = expectations.Missing() 127 missingReport.NumEventsChecked = i 128 return missingReport, nil 129 } 130 131 // CheckAuditList searches an audit event list for the expected audit events. 132 func CheckAuditList(el auditinternal.EventList, expected []AuditEvent) (missing []AuditEvent, err error) { 133 expectations := newAuditEventTracker(expected) 134 135 for _, e := range el.Items { 136 event, err := testEventFromInternal(&e) 137 if err != nil { 138 return expected, err 139 } 140 141 expectations.Mark(event) 142 } 143 144 return expectations.Missing(), nil 145 } 146 147 // CheckForDuplicates checks a list for duplicate events 148 func CheckForDuplicates(el auditinternal.EventList) (auditinternal.EventList, error) { 149 // existingEvents holds a slice of audit events that have been seen 150 existingEvents := []AuditEvent{} 151 duplicates := auditinternal.EventList{} 152 for _, e := range el.Items { 153 event, err := testEventFromInternal(&e) 154 if err != nil { 155 return duplicates, err 156 } 157 event.ID = e.AuditID 158 for _, existing := range existingEvents { 159 if reflect.DeepEqual(existing, event) { 160 duplicates.Items = append(duplicates.Items, e) 161 continue 162 } 163 } 164 existingEvents = append(existingEvents, event) 165 } 166 167 var err error 168 if len(duplicates.Items) > 0 { 169 err = fmt.Errorf("failed duplicate check") 170 } 171 172 return duplicates, err 173 } 174 175 // testEventFromInternal takes an internal audit event and returns a test event 176 func testEventFromInternal(e *auditinternal.Event) (AuditEvent, error) { 177 return testEventFromInternalFiltered(e, nil) 178 } 179 180 // testEventFromInternalFiltered takes an internal audit event and returns a test event, customAnnotationsFilter 181 // controls which audit annotations are added to AuditEvent.CustomAuditAnnotations. 182 // If the customAnnotationsFilter is nil, AuditEvent.CustomAuditAnnotations will be empty. 183 func testEventFromInternalFiltered(e *auditinternal.Event, customAnnotationsFilter AuditAnnotationsFilter) (AuditEvent, error) { 184 event := AuditEvent{ 185 Level: e.Level, 186 Stage: e.Stage, 187 RequestURI: e.RequestURI, 188 Verb: e.Verb, 189 User: e.User.Username, 190 } 191 if e.ObjectRef != nil { 192 event.Namespace = e.ObjectRef.Namespace 193 event.Resource = e.ObjectRef.Resource 194 } 195 if e.ResponseStatus != nil { 196 event.Code = e.ResponseStatus.Code 197 } 198 if e.ResponseObject != nil { 199 event.ResponseObject = true 200 } 201 if e.RequestObject != nil { 202 event.RequestObject = true 203 } 204 if e.ImpersonatedUser != nil { 205 event.ImpersonatedUser = e.ImpersonatedUser.Username 206 sort.Strings(e.ImpersonatedUser.Groups) 207 event.ImpersonatedGroups = strings.Join(e.ImpersonatedUser.Groups, ",") 208 } 209 event.AuthorizeDecision = e.Annotations["authorization.k8s.io/decision"] 210 for k, v := range e.Annotations { 211 if strings.HasPrefix(k, mutating.PatchAuditAnnotationPrefix) { 212 if event.AdmissionWebhookPatchAnnotations == nil { 213 event.AdmissionWebhookPatchAnnotations = map[string]string{} 214 } 215 event.AdmissionWebhookPatchAnnotations[k] = v 216 } else if strings.HasPrefix(k, mutating.MutationAuditAnnotationPrefix) { 217 if event.AdmissionWebhookMutationAnnotations == nil { 218 event.AdmissionWebhookMutationAnnotations = map[string]string{} 219 } 220 event.AdmissionWebhookMutationAnnotations[k] = v 221 } else if customAnnotationsFilter != nil && customAnnotationsFilter(k, v) { 222 if event.CustomAuditAnnotations == nil { 223 event.CustomAuditAnnotations = map[string]string{} 224 } 225 event.CustomAuditAnnotations[k] = v 226 } 227 } 228 return event, nil 229 } 230 231 // auditEvent is a private wrapper on top of AuditEvent used by auditEventTracker 232 type auditEvent struct { 233 event AuditEvent 234 found bool 235 } 236 237 // auditEventTracker keeps track of AuditEvent expectations and marks matching events as found 238 type auditEventTracker struct { 239 events []*auditEvent 240 } 241 242 // newAuditEventTracker creates a tracker that tracks whether expect events are found 243 func newAuditEventTracker(expected []AuditEvent) *auditEventTracker { 244 expectations := &auditEventTracker{events: []*auditEvent{}} 245 for _, event := range expected { 246 // we copy the references to the maps in event 247 expectations.events = append(expectations.events, &auditEvent{event: event, found: false}) 248 } 249 return expectations 250 } 251 252 // Mark marks the given event as found if it's expected 253 func (t *auditEventTracker) Mark(event AuditEvent) { 254 for _, e := range t.events { 255 if reflect.DeepEqual(e.event, event) { 256 e.found = true 257 } 258 } 259 } 260 261 // Missing reports events that are expected but not found 262 func (t *auditEventTracker) Missing() []AuditEvent { 263 var missing []AuditEvent 264 for _, e := range t.events { 265 if !e.found { 266 missing = append(missing, e.event) 267 } 268 } 269 return missing 270 }