-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathredact.go
More file actions
172 lines (159 loc) · 4.71 KB
/
Copy pathredact.go
File metadata and controls
172 lines (159 loc) · 4.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
package flashduty
import (
"encoding/json"
"fmt"
"net/url"
"strings"
)
const (
// defaultMaxLogBodySize is the maximum size of a body before it is truncated in logs.
defaultMaxLogBodySize = 2048
// defaultLogPreviewSize is the size of the preview shown for truncated log content.
defaultLogPreviewSize = 500
)
// sanitizeURL removes sensitive query parameters from a URL for safe logging.
func sanitizeURL(u *url.URL) string {
sanitized := *u
q := sanitized.Query()
if q.Has("app_key") {
q.Set("app_key", "[REDACTED]")
sanitized.RawQuery = q.Encode()
}
return sanitized.String()
}
// sensitiveBodyKeys enumerates normalized JSON keys whose values must be
// redacted before bodies are logged. The set covers common credential aliases
// seen in API payloads and echoed error responses.
var sensitiveBodyKeys = map[string]struct{}{
"apikey": {}, "xapikey": {}, "accesskey": {}, "password": {}, "passwd": {}, "pwd": {},
"token": {}, "accesstoken": {}, "refreshtoken": {}, "idtoken": {}, "sessiontoken": {},
"authtoken": {}, "oauthtoken": {}, "bearertoken": {}, "authorization": {}, "auth": {},
"secret": {}, "clientsecret": {}, "secretkey": {}, "privatekey": {}, "signingkey": {},
"credential": {}, "credentials": {},
}
// redactChildrenKeys enumerates normalized JSON keys whose nested values are
// always redacted regardless of inner key name. These containers (env, headers)
// hold user-chosen keys that frequently carry credentials, so the allow-list
// approach in sensitiveBodyKeys cannot catch them.
var redactChildrenKeys = map[string]struct{}{
"env": {},
"headers": {},
}
// sanitizeBody redacts values of well-known sensitive JSON keys so that secrets
// do not appear in request/response logs. It is best-effort: empty or non-JSON
// bodies pass through unchanged.
func sanitizeBody(body string) string {
if body == "" {
return body
}
var v any
if err := json.Unmarshal([]byte(body), &v); err != nil {
return body
}
sanitized, redacted := sanitizeJSONValue(v)
if !redacted {
return body
}
out, err := json.Marshal(sanitized)
if err != nil {
return body
}
return string(out)
}
func sanitizeJSONValue(v any) (any, bool) {
switch value := v.(type) {
case map[string]any:
sanitized := make(map[string]any, len(value))
redacted := false
for key, item := range value {
if isSensitiveBodyKey(key) {
sanitized[key] = "[REDACTED]"
redacted = true
continue
}
if shouldRedactChildren(key) {
sanitized[key] = redactAllLeaves(item)
redacted = true
continue
}
sanitizedItem, itemRedacted := sanitizeJSONValue(item)
sanitized[key] = sanitizedItem
redacted = redacted || itemRedacted
}
return sanitized, redacted
case []any:
sanitized := make([]any, len(value))
redacted := false
for i, item := range value {
sanitizedItem, itemRedacted := sanitizeJSONValue(item)
sanitized[i] = sanitizedItem
redacted = redacted || itemRedacted
}
return sanitized, redacted
default:
return v, false
}
}
// redactAllLeaves walks v and replaces every non-container leaf with
// "[REDACTED]", preserving the surrounding map/slice shape.
func redactAllLeaves(v any) any {
switch value := v.(type) {
case map[string]any:
out := make(map[string]any, len(value))
for key, item := range value {
out[key] = redactAllLeaves(item)
}
return out
case []any:
out := make([]any, len(value))
for i, item := range value {
out[i] = redactAllLeaves(item)
}
return out
default:
return "[REDACTED]"
}
}
func isSensitiveBodyKey(key string) bool {
_, ok := sensitiveBodyKeys[normalizeSensitiveBodyKey(key)]
return ok
}
func shouldRedactChildren(key string) bool {
_, ok := redactChildrenKeys[normalizeSensitiveBodyKey(key)]
return ok
}
func normalizeSensitiveBodyKey(key string) string {
var b strings.Builder
b.Grow(len(key))
for _, r := range strings.ToLower(strings.TrimSpace(key)) {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
b.WriteRune(r)
}
}
return b.String()
}
// sanitizeError removes a potential app_key-bearing URL from error messages.
func sanitizeError(err error) string {
errStr := err.Error()
idx := strings.Index(errStr, "app_key=")
if idx == -1 {
return errStr
}
endIdx := strings.IndexAny(errStr[idx:], "& ")
if endIdx == -1 {
return errStr[:idx] + "app_key=[REDACTED]"
}
return errStr[:idx] + "app_key=[REDACTED]" + errStr[idx+endIdx:]
}
// truncateBody truncates a body string if it exceeds the default max log size.
func truncateBody(body string) string {
bodyLen := len(body)
if bodyLen <= defaultMaxLogBodySize {
return body
}
previewSize := defaultLogPreviewSize
if previewSize > bodyLen {
previewSize = bodyLen
}
return fmt.Sprintf("[LARGE_BODY: truncated, size: %d bytes, preview: %s...]", bodyLen, body[:previewSize])
}