diff --git a/staging/src/k8s.io/component-base/logs/BUILD b/staging/src/k8s.io/component-base/logs/BUILD index 51d95f3523f..8b71cfaa24b 100644 --- a/staging/src/k8s.io/component-base/logs/BUILD +++ b/staging/src/k8s.io/component-base/logs/BUILD @@ -34,6 +34,7 @@ filegroup( name = "all-srcs", srcs = [ ":package-srcs", + "//staging/src/k8s.io/component-base/logs/datapol:all-srcs", "//staging/src/k8s.io/component-base/logs/json:all-srcs", "//staging/src/k8s.io/component-base/logs/logreduction:all-srcs", ], diff --git a/staging/src/k8s.io/component-base/logs/datapol/BUILD b/staging/src/k8s.io/component-base/logs/datapol/BUILD new file mode 100644 index 00000000000..4f08886ddec --- /dev/null +++ b/staging/src/k8s.io/component-base/logs/datapol/BUILD @@ -0,0 +1,37 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "datapol.go", + "externaltypes.go", + ], + importmap = "k8s.io/kubernetes/vendor/k8s.io/component-base/logs/datapol", + importpath = "k8s.io/component-base/logs/datapol", + visibility = ["//visibility:public"], + deps = ["//vendor/k8s.io/klog/v2:go_default_library"], +) + +go_test( + name = "go_default_test", + srcs = [ + "datapol_test.go", + "externaltypes_test.go", + ], + embed = [":go_default_library"], + deps = ["//vendor/github.com/stretchr/testify/assert:go_default_library"], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/staging/src/k8s.io/component-base/logs/datapol/datapol.go b/staging/src/k8s.io/component-base/logs/datapol/datapol.go new file mode 100644 index 00000000000..6ec8fb12d39 --- /dev/null +++ b/staging/src/k8s.io/component-base/logs/datapol/datapol.go @@ -0,0 +1,99 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package datapol contains functions to determine if objects contain sensitive +// data to e.g. make decisions on whether to log them or not. +package datapol + +import ( + "reflect" + "strings" + + "k8s.io/klog/v2" +) + +// Verify returns a list of the datatypes contained in the argument that can be +// considered sensitive w.r.t. to logging +func Verify(value interface{}) []string { + defer func() { + if r := recover(); r != nil { + //TODO maybe export a metric + klog.Warningf("Error while inspecting arguments for sensitive data: %v", r) + } + }() + t := reflect.ValueOf(value) + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + return datatypes(t) +} + +func datatypes(v reflect.Value) []string { + if types := byType(v.Type()); len(types) > 0 { + // Slices, and maps can be nil or empty, only the nil case is zero + switch v.Kind() { + case reflect.Slice, reflect.Map: + if !v.IsZero() && v.Len() > 0 { + return types + } + default: + if !v.IsZero() { + return types + } + } + } + switch v.Kind() { + case reflect.Interface: + return datatypes(v.Elem()) + case reflect.Slice, reflect.Array: + for i := 0; i < v.Len(); i++ { + if types := datatypes(v.Index(i)); len(types) > 0 { + return types + } + } + case reflect.Map: + mapIter := v.MapRange() + for mapIter.Next() { + k := mapIter.Key() + v := mapIter.Value() + if types := datatypes(k); len(types) > 0 { + return types + } + if types := datatypes(v); len(types) > 0 { + return types + } + } + case reflect.Struct: + t := v.Type() + numField := t.NumField() + + for i := 0; i < numField; i++ { + f := t.Field(i) + if f.Type.Kind() == reflect.Ptr { + continue + } + if reason, ok := f.Tag.Lookup("datapolicy"); ok { + if !v.Field(i).IsZero() { + return strings.Split(reason, ",") + } + } + if types := datatypes(v.Field(i)); len(types) > 0 { + return types + } + } + } + return nil +} diff --git a/staging/src/k8s.io/component-base/logs/datapol/datapol_test.go b/staging/src/k8s.io/component-base/logs/datapol/datapol_test.go new file mode 100644 index 00000000000..f792cca7d6e --- /dev/null +++ b/staging/src/k8s.io/component-base/logs/datapol/datapol_test.go @@ -0,0 +1,152 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package datapol + +import ( + "fmt" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + marker = "hunter2" +) + +type withDatapolTag struct { + Key string `json:"key" datapolicy:"password"` +} + +type withExternalType struct { + Header http.Header `json:"header"` +} + +type noDatapol struct { + Key string `json:"key"` +} + +type datapolInMember struct { + secrets withDatapolTag +} + +type datapolInSlice struct { + secrets []withDatapolTag +} + +type datapolInMap struct { + secrets map[string]withDatapolTag +} + +type datapolBehindPointer struct { + secrets *withDatapolTag +} + +func TestValidate(t *testing.T) { + testcases := []struct { + name string + value interface{} + expect []string + badFilter bool + }{{ + name: "Empty password", + value: withDatapolTag{}, + expect: []string{}, + }, { + name: "Non-empty password", + value: withDatapolTag{ + Key: marker, + }, + expect: []string{"password"}, + }, { + name: "empty external type", + value: withExternalType{Header: http.Header{}}, + expect: []string{}, + }, { + name: "external type", + value: withExternalType{Header: http.Header{ + "Authorization": []string{"Bearer hunter2"}, + }}, + expect: []string{"password", "token"}, + }, { + name: "no datapol tag", + value: noDatapol{Key: marker}, + expect: []string{}, + badFilter: true, + }, { + name: "nested", + value: datapolInMember{ + secrets: withDatapolTag{ + Key: marker, + }, + }, + expect: []string{"password"}, + }, { + name: "nested in pointer", + value: datapolBehindPointer{ + secrets: &withDatapolTag{Key: marker}, + }, + expect: []string{}, + }, { + name: "nested in slice", + value: datapolInSlice{ + secrets: []withDatapolTag{{Key: marker}}, + }, + expect: []string{"password"}, + }, { + name: "nested in map", + value: datapolInMap{ + secrets: map[string]withDatapolTag{ + "key": {Key: marker}, + }, + }, + expect: []string{"password"}, + }, { + name: "nested in map but empty", + value: datapolInMap{ + secrets: map[string]withDatapolTag{ + "key": {}, + }, + }, + expect: []string{}, + }, { + name: "struct in interface", + value: struct{ v interface{} }{v: withDatapolTag{ + Key: marker, + }}, + expect: []string{"password"}, + }, { + name: "structptr in interface", + value: struct{ v interface{} }{v: &withDatapolTag{ + Key: marker, + }}, + expect: []string{}, + }} + for _, tc := range testcases { + res := Verify(tc.value) + if !assert.ElementsMatch(t, tc.expect, res) { + t.Errorf("Wrong set of tags for %q. expect %v, got %v", tc.name, tc.expect, res) + } + if !tc.badFilter { + formatted := fmt.Sprintf("%v", tc.value) + if strings.Contains(formatted, marker) != (len(tc.expect) > 0) { + t.Errorf("Filter decision doesn't match formatted value for %q: tags: %v, format: %s", tc.name, tc.expect, formatted) + } + } + } +} diff --git a/staging/src/k8s.io/component-base/logs/datapol/externaltypes.go b/staging/src/k8s.io/component-base/logs/datapol/externaltypes.go new file mode 100644 index 00000000000..d343d3e6591 --- /dev/null +++ b/staging/src/k8s.io/component-base/logs/datapol/externaltypes.go @@ -0,0 +1,49 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package datapol + +import ( + "fmt" + "reflect" +) + +const ( + httpHeader = "net/http.Header" + httpCookie = "net/http.Cookie" + x509Certificate = "crypto/x509.Certificate" +) + +// GlobalDatapolicyMapping returns the list of sensitive datatypes are embedded +// in types not native to Kubernetes. +func GlobalDatapolicyMapping(v interface{}) []string { + return byType(reflect.TypeOf(v)) +} + +func byType(t reflect.Type) []string { + // Use string representation of the type to prevent taking a depency on the actual type. + switch fmt.Sprintf("%s.%s", t.PkgPath(), t.Name()) { + case httpHeader: + return []string{"password", "token"} + case httpCookie: + return []string{"token"} + case x509Certificate: + return []string{"security-key"} + default: + return nil + } + +} diff --git a/staging/src/k8s.io/component-base/logs/datapol/externaltypes_test.go b/staging/src/k8s.io/component-base/logs/datapol/externaltypes_test.go new file mode 100644 index 00000000000..b0a4ab722a7 --- /dev/null +++ b/staging/src/k8s.io/component-base/logs/datapol/externaltypes_test.go @@ -0,0 +1,47 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package datapol + +import ( + "crypto/x509" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTypes(t *testing.T) { + testcases := []struct { + value interface{} + expect []string + }{{ + value: http.Header{}, + expect: []string{"password", "token"}, + }, { + value: http.Cookie{}, + expect: []string{"token"}, + }, { + value: x509.Certificate{}, + expect: []string{"security-key"}, + }} + for _, tc := range testcases { + types := GlobalDatapolicyMapping(tc.value) + if !assert.ElementsMatch(t, tc.expect, types) { + t.Errorf("Wrong set of datatypes detected for %T, want: %v, got %v", tc.value, tc.expect, types) + } + } +}