|
| 1 | +// Licensed to Elasticsearch B.V. under one or more contributor |
| 2 | +// license agreements. See the NOTICE file distributed with |
| 3 | +// this work for additional information regarding copyright |
| 4 | +// ownership. Elasticsearch B.V. licenses this file to you under |
| 5 | +// the Apache License, Version 2.0 (the "License"); you may |
| 6 | +// not use this file except in compliance with the License. |
| 7 | +// You may obtain a copy of the License at |
| 8 | +// |
| 9 | +// http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | +// |
| 11 | +// Unless required by applicable law or agreed to in writing, |
| 12 | +// software distributed under the License is distributed on an |
| 13 | +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| 14 | +// KIND, either express or implied. See the License for the |
| 15 | +// specific language governing permissions and limitations |
| 16 | +// under the License. |
| 17 | + |
| 18 | +package redact |
| 19 | + |
| 20 | +import ( |
| 21 | + "fmt" |
| 22 | + "io" |
| 23 | + "net/url" |
| 24 | + "reflect" |
| 25 | + "slices" |
| 26 | + "strings" |
| 27 | +) |
| 28 | + |
| 29 | +// REDACTED is the value that replaces sensitive values. |
| 30 | +const REDACTED = "REDACTED" |
| 31 | + |
| 32 | +// RedactOption is an optional arg to control how the Redact method works. |
| 33 | +type RedactOption func(ro *redactOptions) |
| 34 | + |
| 35 | +type redactOptions struct { |
| 36 | + errOut io.Writer |
| 37 | + markerPrefix string |
| 38 | + ignoreKeys []string |
| 39 | +} |
| 40 | + |
| 41 | +// WithErrorOutput determines where any error messages that are encountered are written to. |
| 42 | +// |
| 43 | +// Defaults to io.Discard. |
| 44 | +func WithErrorOutput(w io.Writer) RedactOption { |
| 45 | + return func(ro *redactOptions) { |
| 46 | + ro.errOut = w |
| 47 | + } |
| 48 | +} |
| 49 | + |
| 50 | +// WithMarkerPrefix sets the redaction marker prefix string. |
| 51 | +func WithMarkerPrefix(s string) RedactOption { |
| 52 | + return func(ro *redactOptions) { |
| 53 | + ro.markerPrefix = s |
| 54 | + } |
| 55 | +} |
| 56 | + |
| 57 | +// WithIgnoreKeys sets case-sensitive key values where redaction will be skipped. |
| 58 | +func WithIgnoreKeys(keys ...string) RedactOption { |
| 59 | + return func(ro *redactOptions) { |
| 60 | + ro.ignoreKeys = append(ro.ignoreKeys, keys...) |
| 61 | + } |
| 62 | +} |
| 63 | + |
| 64 | +// Redact walks obj and replaces values of sensitive keys with a redacted |
| 65 | +// placeholder. It mutates obj in place; nested maps and slice elements are |
| 66 | +// modified directly and no copy is returned. |
| 67 | +func Redact(obj map[any]any, opts ...RedactOption) { |
| 68 | + ro := &redactOptions{ |
| 69 | + errOut: io.Discard, |
| 70 | + markerPrefix: "", |
| 71 | + ignoreKeys: []string{}, |
| 72 | + } |
| 73 | + for _, opt := range opts { |
| 74 | + opt(ro) |
| 75 | + } |
| 76 | + redactMap(obj, ro) |
| 77 | +} |
| 78 | + |
| 79 | +func redactMap[K comparable](obj map[K]any, ro *redactOptions) { |
| 80 | + if obj == nil { |
| 81 | + return |
| 82 | + } |
| 83 | + |
| 84 | + markers := make([]string, 0) |
| 85 | + for key, val := range obj { |
| 86 | + // detect if the obj has entries of the form: |
| 87 | + // - name: Authorization |
| 88 | + // value: Bearer SecretValue |
| 89 | + if keyString, ok := any(key).(string); ok && strings.ToLower(keyString) == "name" { |
| 90 | + keyVal, ok := val.(string) |
| 91 | + if ok && redactKey(keyVal, ro) { |
| 92 | + for vk := range obj { |
| 93 | + if vs, ok := any(vk).(string); ok && strings.ToLower(vs) == "value" { |
| 94 | + obj[vk] = REDACTED |
| 95 | + break |
| 96 | + } |
| 97 | + } |
| 98 | + } |
| 99 | + } |
| 100 | + if val != nil { |
| 101 | + switch cast := val.(type) { |
| 102 | + case map[string]any: |
| 103 | + redactMap(cast, ro) |
| 104 | + case map[any]any: |
| 105 | + redactMap(cast, ro) |
| 106 | + case map[int]any: |
| 107 | + redactMap(cast, ro) |
| 108 | + case []any: |
| 109 | + // Recursively process each element in the slice so that we also walk |
| 110 | + // through lists (e.g. inputs[4].streams[0]). This is required to |
| 111 | + // reach redaction markers that are inside slice items. |
| 112 | + for i, value := range cast { |
| 113 | + switch m := value.(type) { |
| 114 | + case map[string]any: |
| 115 | + redactMap(m, ro) |
| 116 | + case map[any]any: |
| 117 | + redactMap(m, ro) |
| 118 | + case map[int]any: |
| 119 | + redactMap(m, ro) |
| 120 | + case string: |
| 121 | + if redactedValue, redact := redactURL(m); redact { |
| 122 | + cast[i] = redactedValue |
| 123 | + } |
| 124 | + } |
| 125 | + } |
| 126 | + case string: |
| 127 | + if keyString, ok := any(key).(string); ok && redactKey(keyString, ro) { |
| 128 | + val = REDACTED |
| 129 | + } else if redactedValue, redact := redactURL(cast); redact { |
| 130 | + val = redactedValue |
| 131 | + } |
| 132 | + case bool: // redaction marker values are always going to be bool, process redaction markers in this case |
| 133 | + if keyString, ok := any(key).(string); ok { |
| 134 | + // Find siblings that have the redaction marker. |
| 135 | + if ro.markerPrefix != "" && strings.HasPrefix(keyString, ro.markerPrefix) { |
| 136 | + markers = append(markers, keyString) |
| 137 | + delete(obj, key) |
| 138 | + continue |
| 139 | + } |
| 140 | + } |
| 141 | + default: |
| 142 | + // in cases where we got some weird kind of map we couldn't parse, print a warning |
| 143 | + if reflect.TypeOf(val).Kind() == reflect.Map { |
| 144 | + fmt.Fprintf(ro.errOut, "[WARNING]: file may be partly redacted, could not cast value %v of type %T", key, val) |
| 145 | + } |
| 146 | + |
| 147 | + } |
| 148 | + } |
| 149 | + obj[key] = val |
| 150 | + } |
| 151 | + |
| 152 | + for _, redactionMarker := range markers { |
| 153 | + keyToRedact := strings.TrimPrefix(redactionMarker, ro.markerPrefix) |
| 154 | + for rootKey := range obj { |
| 155 | + if keyString, ok := any(rootKey).(string); ok { |
| 156 | + if keyString == keyToRedact { |
| 157 | + obj[rootKey] = REDACTED |
| 158 | + } |
| 159 | + |
| 160 | + if keyString == redactionMarker { |
| 161 | + delete(obj, rootKey) |
| 162 | + } |
| 163 | + } |
| 164 | + } |
| 165 | + } |
| 166 | +} |
| 167 | + |
| 168 | +func redactKey(k string, ro *redactOptions) bool { |
| 169 | + if slices.Contains(ro.ignoreKeys, k) { |
| 170 | + return false |
| 171 | + } |
| 172 | + |
| 173 | + k = strings.ToLower(k) |
| 174 | + return strings.Contains(k, "auth") || |
| 175 | + strings.Contains(k, "certificate") || |
| 176 | + strings.Contains(k, "passphrase") || |
| 177 | + strings.Contains(k, "password") || |
| 178 | + strings.Contains(k, "token") || |
| 179 | + strings.Contains(k, "key") || |
| 180 | + strings.Contains(k, "secret") |
| 181 | +} |
| 182 | + |
| 183 | +func redactURL(v string) (string, bool) { |
| 184 | + u, err := url.Parse(v) |
| 185 | + |
| 186 | + if err != nil || u.User == nil { |
| 187 | + return v, false |
| 188 | + } |
| 189 | + |
| 190 | + u.User = url.UserPassword(REDACTED, REDACTED) |
| 191 | + |
| 192 | + return u.String(), true |
| 193 | +} |
0 commit comments