diff --git a/staging/src/k8s.io/component-base/logs/BUILD b/staging/src/k8s.io/component-base/logs/BUILD index 8b71cfaa24b..aac0b455819 100644 --- a/staging/src/k8s.io/component-base/logs/BUILD +++ b/staging/src/k8s.io/component-base/logs/BUILD @@ -37,6 +37,7 @@ filegroup( "//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", + "//staging/src/k8s.io/component-base/logs/sanitization:all-srcs", ], tags = ["automanaged"], ) diff --git a/staging/src/k8s.io/component-base/logs/sanitization/BUILD b/staging/src/k8s.io/component-base/logs/sanitization/BUILD new file mode 100644 index 00000000000..7369331bbb2 --- /dev/null +++ b/staging/src/k8s.io/component-base/logs/sanitization/BUILD @@ -0,0 +1,31 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["sanitization.go"], + importmap = "k8s.io/kubernetes/vendor/k8s.io/component-base/logs/sanitization", + importpath = "k8s.io/component-base/logs/sanitization", + visibility = ["//visibility:public"], + deps = ["//staging/src/k8s.io/component-base/logs/datapol: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"], +) + +go_test( + name = "go_default_test", + srcs = ["sanitization_test.go"], + embed = [":go_default_library"], + deps = ["//vendor/github.com/stretchr/testify/assert:go_default_library"], +) diff --git a/staging/src/k8s.io/component-base/logs/sanitization/sanitization.go b/staging/src/k8s.io/component-base/logs/sanitization/sanitization.go new file mode 100644 index 00000000000..0d1b7d52f42 --- /dev/null +++ b/staging/src/k8s.io/component-base/logs/sanitization/sanitization.go @@ -0,0 +1,69 @@ +/* +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 sanitization + +import ( + "fmt" + + "k8s.io/component-base/logs/datapol" +) + +const ( + datapolMsgFmt = "Log message has been redacted. Log argument #%d contains: %v" + datapolMsg = "Log message has been redacted." +) + +// SanitizingFilter implements the LogFilter interface from klog with a set of functions that inspects the arguments with the datapol library +type SanitizingFilter struct{} + +// Filter is the filter function for the non-formatting logging functions of klog. +func (sf *SanitizingFilter) Filter(args []interface{}) []interface{} { + for i, v := range args { + types := datapol.Verify(v) + if len(types) > 0 { + return []interface{}{fmt.Sprintf(datapolMsgFmt, i, types)} + } + } + return args +} + +// FilterF is the filter function for the formatting logging functions of klog +func (sf *SanitizingFilter) FilterF(fmt string, args []interface{}) (string, []interface{}) { + for i, v := range args { + types := datapol.Verify(v) + if len(types) > 0 { + return datapolMsgFmt, []interface{}{i, types} + } + } + return fmt, args + +} + +// FilterS is the filter for the structured logging functions of klog. +func (sf *SanitizingFilter) FilterS(msg string, keysAndValues []interface{}) (string, []interface{}) { + for i, v := range keysAndValues { + types := datapol.Verify(v) + if len(types) > 0 { + if i%2 == 0 { + return datapolMsg, []interface{}{"key_index", i, "types", types} + } + // since we scanned linearly we can safely log the key. + return datapolMsg, []interface{}{"key", keysAndValues[i-1], "types", types} + } + } + return msg, keysAndValues +} diff --git a/staging/src/k8s.io/component-base/logs/sanitization/sanitization_test.go b/staging/src/k8s.io/component-base/logs/sanitization/sanitization_test.go new file mode 100644 index 00000000000..e79c615015f --- /dev/null +++ b/staging/src/k8s.io/component-base/logs/sanitization/sanitization_test.go @@ -0,0 +1,199 @@ +/* +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 sanitization + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type withDatapolTag struct { + Key string `json:"key" datapolicy:"password"` +} + +func datapolItem() interface{} { + return withDatapolTag{Key: "hunter2"} +} + +func TestFilter(t *testing.T) { + filter := &SanitizingFilter{} + testcases := []struct { + input []interface{} + output []interface{} + }{{ + input: []interface{}{}, + output: []interface{}{}, + }, { + input: []interface{}{ + "nothing special", "really", + }, + output: []interface{}{ + "nothing special", "really", + }, + }, { + input: []interface{}{ + datapolItem(), + }, + output: []interface{}{ + "Log message has been redacted. Log argument #0 contains: [password]", + }, + }, { + input: []interface{}{ + "nothing special", datapolItem(), + }, + output: []interface{}{ + "Log message has been redacted. Log argument #1 contains: [password]", + }, + }} + for _, tc := range testcases { + output := filter.Filter(tc.input) + if !assert.ElementsMatch(t, tc.output, output) { + t.Errorf("Unexpected filter output for %v, want: %v, got %v", tc.input, tc.output, output) + } + } +} + +func TestFilterF(t *testing.T) { + filter := &SanitizingFilter{} + testcases := []struct { + inputFmt string + input []interface{} + outputFmt string + output []interface{} + }{{ + inputFmt: "", + input: []interface{}{}, + outputFmt: "", + output: []interface{}{}, + }, { + inputFmt: "%s: %s", + input: []interface{}{ + "nothing special", "really", + }, + outputFmt: "%s: %s", + output: []interface{}{ + "nothing special", "really", + }, + }, { + inputFmt: "%v", + input: []interface{}{ + datapolItem(), + }, + outputFmt: "Log message has been redacted. Log argument #%d contains: %v", + output: []interface{}{ + 0, []string{"password"}, + }, + }, { + inputFmt: "%v", + input: []interface{}{ + "nothing special", datapolItem(), + }, + outputFmt: "Log message has been redacted. Log argument #%d contains: %v", + output: []interface{}{ + 1, []string{"password"}, + }, + }} + for _, tc := range testcases { + outputFmt, output := filter.FilterF(tc.inputFmt, tc.input) + correctFmt := outputFmt == tc.outputFmt + correctArgs := assert.ElementsMatch(t, tc.output, output) + if !correctFmt || !correctArgs { + t.Errorf("Error while executing testcase %s, %v", tc.inputFmt, tc.input) + } + if !correctFmt { + t.Errorf("Unexpected output format string want %v, got %v", tc.outputFmt, outputFmt) + } + if !correctArgs { + t.Errorf("Unexpected filter output arguments want: %v, got %v", tc.output, output) + } + } +} + +func TestFilterS(t *testing.T) { + filter := &SanitizingFilter{} + testcases := []struct { + inputMsg string + input []interface{} + outputMsg string + output []interface{} + }{{ + inputMsg: "", + input: []interface{}{}, + outputMsg: "", + output: []interface{}{}, + }, { + inputMsg: "Message", + input: []interface{}{ + "nothing special", "really", + }, + outputMsg: "Message", + output: []interface{}{ + "nothing special", "really", + }, + }, { + inputMsg: "%v", + input: []interface{}{ + datapolItem(), "value1", "key2", "value2", + }, + outputMsg: "Log message has been redacted.", + output: []interface{}{ + "key_index", 0, "types", []string{"password"}, + }, + }, { + inputMsg: "%v", + input: []interface{}{ + "key1", "value1", datapolItem(), "value2", + }, + outputMsg: "Log message has been redacted.", + output: []interface{}{ + "key_index", 2, "types", []string{"password"}, + }, + }, { + inputMsg: "%v", + input: []interface{}{ + "key1", datapolItem(), "key2", "value2", + }, + outputMsg: "Log message has been redacted.", + output: []interface{}{ + "key", "key1", "types", []string{"password"}, + }, + }, { + inputMsg: "%v", + input: []interface{}{ + "key1", "value1", "key2", datapolItem(), + }, + outputMsg: "Log message has been redacted.", + output: []interface{}{ + "key", "key2", "types", []string{"password"}, + }, + }} + for _, tc := range testcases { + outputMsg, output := filter.FilterS(tc.inputMsg, tc.input) + correctMsg := outputMsg == tc.outputMsg + correctArgs := assert.ElementsMatch(t, tc.output, output) + if !correctMsg || !correctArgs { + t.Errorf("Error while executing testcase %s, %v", tc.inputMsg, tc.input) + } + if !correctMsg { + t.Errorf("Unexpected output format string want %v, got %v", tc.outputMsg, outputMsg) + } + if !correctArgs { + t.Errorf("Unexpected filter output arguments want: %v, got %v", tc.output, output) + } + } +}