Skip to content

Commit

Permalink
Redact passwords in json that goes to the log. (#441)
Browse files Browse the repository at this point in the history
  • Loading branch information
MatthewHink authored Dec 11, 2019
1 parent b79b1b2 commit d90486a
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 4 deletions.
2 changes: 1 addition & 1 deletion sdk/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ func (device *Device) JSON() (string, error) {
if err != nil {
return "", err
}
return string(bytes), nil
return RedactPasswords(string(bytes))
}

// GetType gets the type of the device. The type of the device is the last
Expand Down
4 changes: 2 additions & 2 deletions sdk/device_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ func TestDevice_JSON_1(t *testing.T) {
assert.NoError(t, err)
assert.Equal(
t,
`{"Kind":"","Metadata":null,"Plugin":"","Info":"","Location":null,"Data":null,"Outputs":null,"SortOrdinal":0}`,
"{\"Data\":null,\"Info\":\"\",\"Kind\":\"\",\"Location\":null,\"Metadata\":null,\"Outputs\":null,\"Plugin\":\"\",\"SortOrdinal\":0}",
out,
)
}
Expand All @@ -206,7 +206,7 @@ func TestDevice_JSON_2(t *testing.T) {
assert.NoError(t, err)
assert.Equal(
t,
`{"Kind":"foo","Metadata":{"test":"data"},"Plugin":"","Info":"info","Location":{"Rack":"rack","Board":"board"},"Data":null,"Outputs":null,"SortOrdinal":1}`,
"{\"Data\":null,\"Info\":\"info\",\"Kind\":\"foo\",\"Location\":{\"Board\":\"board\",\"Rack\":\"rack\"},\"Metadata\":{\"test\":\"data\"},\"Outputs\":null,\"Plugin\":\"\",\"SortOrdinal\":1}",
out,
)
}
Expand Down
2 changes: 1 addition & 1 deletion sdk/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ func (config *PluginConfig) JSON() (string, error) {
if err != nil {
return "", err
}
return string(bytes), nil
return RedactPasswords(string(bytes))
}

// Validate validates that the PluginConfig has no configuration errors.
Expand Down
131 changes: 131 additions & 0 deletions sdk/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package sdk

import (
"crypto/md5" // #nosec
"encoding/json"
"fmt"
"io"
"reflect"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -190,3 +192,132 @@ func logStartupInfo() {
}
log.Info("--------------------------------")
}

// RedactPasswords redacts map fields containing key substring "pass"
// (case-insensitive) in Marshaled json string s, and returns a string where
// those field values are emitted as REDACTED.
func RedactPasswords(s string) (output string, err error) {

// Unmarshal json string to structure.
var x interface{}
err = json.Unmarshal([]byte(s), &x)
if err != nil {
return
}

// We only need to be concerned with the following types.
// If someone has a magic string that happens to be a password, we cannot
// help you.
switch v := x.(type) {
case map[string]interface{}:
err = traverseMap(x.(map[string]interface{}))
case []interface{}:
err = traverseSlice(x.([]interface{}))
default:
err = fmt.Errorf("Unsupported type: %v\n", v)
}

if err != nil {
fmt.Printf("Error traversing: %v\n", err)
return
}

// Marshal to bytes. Return string.
outputTemp, err := json.Marshal(x)
if err != nil {
return
}
return string(outputTemp), nil
}

// traverseMap iterates through all keys and values in a map[string]interface{}.
// If it finds a nested map[string]interface{} we recurse into it.
func traverseMap(m map[string]interface{}) (err error) {

for k, v := range m {

// If the key contains the string "pass" (case-insensitive), we substitute
// with the string REDACTED
klower := strings.ToLower(k)
if strings.Contains(klower, "pass") {
// Redact the data whatever it is.
m[k] = "REDACTED"
continue
}

// Is this a map of [string]interface{}?
vvalue := reflect.ValueOf(v)
vkind := vvalue.Kind()
if vkind == reflect.Map {
// Yes this is a map of [string]interface{}
if vvalue.IsNil() {
continue
}
nestedMap, ok := v.(map[string]interface{})
if ok {
err := traverseMap(nestedMap)
if err != nil {
return err
}
}
}

// Is this a []interface{}?
if vkind == reflect.Slice {
// Yes.
if vvalue.IsNil() {
continue
}
nestedSlice, ok := v.([]interface{})
if ok {
err := traverseSlice(nestedSlice)
if err != nil {
return err
}
}
}
}
return nil
}

// traverseSlice iterates through all values in a []interface{}. If it finds a
// nested map[string]interface{} or a []interface we recurse into it.
func traverseSlice(s []interface{}) (err error) {

for i := 0; i < len(s); i++ {
v := s[i]

// Is this a map of [string]interface{}?
vvalue := reflect.ValueOf(v)
vkind := vvalue.Kind()
if vkind == reflect.Map {
// Yes this is a map [string]interface{}
if vvalue.IsNil() {
continue
}
nestedMap, ok := v.(map[string]interface{})
if ok {
err := traverseMap(nestedMap)
if err != nil {
return err
}
}
}

// Is this a []interface{}
if vkind == reflect.Slice {
// Yes.
if vvalue.IsNil() {
continue
}
nestedSlice, ok := v.([]interface{})
if ok {
err := traverseSlice(nestedSlice)
if err != nil {
return err
}
}
}
}
return nil
}
18 changes: 18 additions & 0 deletions sdk/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -487,3 +487,21 @@ func Test_registerDevices4(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, 1, len(ctx.devices))
}

// TestRedactedJsonConfigOutput tests that we redact passwords when we dump
// them to the logs. map[string]interface{} values will be replaced with
// REDACTED if the lowercase map key contains "pass".
func TestRedactedJsonConfigOutput(t *testing.T) {
// Test string from a log before we redacted passwords.
s := "{\"Version\":\"1\",\"Debug\":true,\"Settings\":{\"Mode\":\"serial\",\"Listen\":{\"Enabled\":true,\"Buffer\":100},\"Read\":{\"Enabled\":true,\"Interval\":\"3s\",\"Buffer\":1024},\"Write\":{\"Enabled\":true,\"Interval\":\"1s\",\"Buffer\":1024,\"Max\":100},\"Transaction\":{\"TTL\":\"5m\"},\"Cache\":{\"Enabled\":false,\"TTL\":180000000000}},\"Network\":{\"Type\":\"tcp\",\"Address\":\"0.0.0.0:5003\",\"TLS\":null},\"DynamicRegistration\":{\"Config\":[{\"authenticationPassphrase\":\"dog\",\"authenticationProtocol\":\"SHA\",\"endpoint\":\"10.193.3.201\",\"model\":\"PXGMS UPS + EATON 93PM\",\"port\":161,\"privacyPassphrase\":\"cat\",\"privacyProtocol\":\"AES\",\"userName\":\"user\",\"version\":\"v3\"}]},\"Limiter\":null,\"Health\":{\"UseDefaults\":true},\"Context\":{}}"

expected := `{"Context":{},"Debug":true,"DynamicRegistration":{"Config":[{"authenticationPassphrase":"REDACTED","authenticationProtocol":"SHA","endpoint":"10.193.3.201","model":"PXGMS UPS + EATON 93PM","port":161,"privacyPassphrase":"REDACTED","privacyProtocol":"AES","userName":"user","version":"v3"}]},"Health":{"UseDefaults":true},"Limiter":null,"Network":{"Address":"0.0.0.0:5003","TLS":null,"Type":"tcp"},"Settings":{"Cache":{"Enabled":false,"TTL":180000000000},"Listen":{"Buffer":100,"Enabled":true},"Mode":"serial","Read":{"Buffer":1024,"Enabled":true,"Interval":"3s"},"Transaction":{"TTL":"5m"},"Write":{"Buffer":1024,"Enabled":true,"Interval":"1s","Max":100}},"Version":"1"}`

output, err := RedactPasswords(s)
if err != nil {
fmt.Printf("Error: %v\n", err)
}

assert.NoError(t, err)
assert.Equal(t, expected, output)
}

0 comments on commit d90486a

Please sign in to comment.