Skip to content

Commit 64899f6

Browse files
Add redact.Redact method (#416)
* Add redact.Redact method * Add license headers * Fix linter * Review feedback * fix linter * Update redact/redact.go Co-authored-by: Mauri de Souza Meneguzzo <mauri870@gmail.com> --------- Co-authored-by: Mauri de Souza Meneguzzo <mauri870@gmail.com>
1 parent f7309eb commit 64899f6

2 files changed

Lines changed: 751 additions & 0 deletions

File tree

redact/redact.go

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
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

Comments
 (0)