Skip to content

Commit

Permalink
port password redact feature from master to v3 branch
Browse files Browse the repository at this point in the history
  • Loading branch information
edaniszewski committed Mar 30, 2020
1 parent 69bdc7c commit 7d1bf99
Show file tree
Hide file tree
Showing 3 changed files with 298 additions and 1 deletion.
3 changes: 2 additions & 1 deletion sdk/config/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"time"

log "github.com/sirupsen/logrus"
"github.com/vapor-ware/synse-sdk/sdk/utils"
)

// Plugin contains the configuration for a Synse Plugin.
Expand Down Expand Up @@ -415,7 +416,7 @@ func (conf *DynamicRegistrationSettings) Log() {
log.Infof(" DynamicRegistration: nil")
} else {
log.Infof(" DynamicRegistration:")
log.Infof(" Config: %v", conf.Config)
log.Infof(" Config: %v", utils.RedactPasswords(conf.Config))
}
}

Expand Down
129 changes: 129 additions & 0 deletions sdk/utils/redact.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Synse SDK
// Copyright (c) 2019 Vapor IO
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package utils

import (
"reflect"
"strings"
)

// RedactPasswords redacts any map fields where the key contains the substring
// "pass" (case-insensitive). It traverses through any slice or map within to
// search for fields to redact.
//
// This does not make any attempt to find other potential passwords as
// magic strings. via regex, or via entropy. This is just meant to cover the
// basic case of "pass": "foo" within various config locations where it is
// likely to exist and should not be leaked out into logs.
func RedactPasswords(m interface{}) interface{} {

switch m.(type) {
case map[string]interface{}:
redacted := map[string]interface{}{}
for k, v := range m.(map[string]interface{}) {
redacted[k] = v
}
traverseMap(redacted)
return redacted

case []interface{}:
var redacted []interface{}
for _, v := range m.([]interface{}) {
redacted = append(redacted, v)
}
traverseSlice(redacted)
return redacted

default:
return m
}
}

// traverseMap iterates through all keys and values in a map[string]interface{},
// replacing passwords with a redacted string. If it finds a nested
// map[string]interface{} we recurse into it.
func traverseMap(m map[string]interface{}) {
for k, v := range m {

// If the key contains the string "pass" (case-insensitive), we substitute
// with the string REDACTED
if strings.Contains(strings.ToLower(k), "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 {
traverseMap(nestedMap)
}
}

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

// 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{}) {
for _, v := range s {

// 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 {
traverseMap(nestedMap)
}
}

// Is this a []interface{}
if vkind == reflect.Slice {
// Yes.
if vvalue.IsNil() {
continue
}
nestedSlice, ok := v.([]interface{})
if ok {
traverseSlice(nestedSlice)
}
}
}
}
167 changes: 167 additions & 0 deletions sdk/utils/redact_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Synse SDK
// Copyright (c) 2019 Vapor IO
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package utils

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestRedactPasswords(t *testing.T) {

var nilMap map[string]interface{}
var nilSlice []interface{}

tests := []struct {
name string
input interface{}
expected interface{}
}{
{
name: "nil value",
input: nil,
expected: nil,
},
{
name: "boolean",
input: true,
expected: true,
},
{
name: "string",
input: "test-string",
expected: "test-string",
},
{
name: "map with no password",
input: map[string]interface{}{"key": "value"},
expected: map[string]interface{}{"key": "value"},
},
{
name: "map with key pass, value string",
input: map[string]interface{}{"key": "value", "pass": "foobar"},
expected: map[string]interface{}{"key": "value", "pass": "REDACTED"},
},
{
name: "map with key PASS, value int",
input: map[string]interface{}{"key": "value", "PASS": 123},
expected: map[string]interface{}{"key": "value", "PASS": "REDACTED"},
},
{
name: "map with key Password, value string",
input: map[string]interface{}{"key": "value", "Password": "password"},
expected: map[string]interface{}{"key": "value", "Password": "REDACTED"},
},
{
name: "map with key User Password, value map",
input: map[string]interface{}{"key": "value", "User Password": map[string]interface{}{"foo": "bar"}},
expected: map[string]interface{}{"key": "value", "User Password": "REDACTED"},
},
{
name: "map with key userpass, value slice",
input: map[string]interface{}{"key": "value", "userpass": []interface{}{1, 2, 3}},
expected: map[string]interface{}{"key": "value", "userpass": "REDACTED"},
},
{
name: "map with nested map with password",
input: map[string]interface{}{"key": map[string]interface{}{"key": "value", "pass": "foo"}},
expected: map[string]interface{}{"key": map[string]interface{}{"key": "value", "pass": "REDACTED"}},
},
{
name: "map with nested map with no password",
input: map[string]interface{}{"key": map[string]interface{}{"key": "value", "bar": "foo"}},
expected: map[string]interface{}{"key": map[string]interface{}{"key": "value", "bar": "foo"}},
},
{
name: "map with nested slice with nested map with password",
input: map[string]interface{}{"foo": []interface{}{map[string]interface{}{"pass": "foo", "other": "bar"}}},
expected: map[string]interface{}{"foo": []interface{}{map[string]interface{}{"pass": "REDACTED", "other": "bar"}}},
},
{
name: "map with empty slice",
input: map[string]interface{}{"key": []interface{}{}},
expected: map[string]interface{}{"key": []interface{}{}},
},
{
name: "map with empty map",
input: map[string]interface{}{"key": map[string]interface{}{}},
expected: map[string]interface{}{"key": map[string]interface{}{}},
},
{
name: "map with nil slice",
input: map[string]interface{}{"key": nilSlice},
expected: map[string]interface{}{"key": nilSlice},
},
{
name: "map with nil map",
input: map[string]interface{}{"key": nilMap},
expected: map[string]interface{}{"key": nilMap},
},
{
name: "slice with no maps",
input: []interface{}{1, 2, 3},
expected: []interface{}{1, 2, 3},
},
{
name: "slice with map, no password",
input: []interface{}{map[string]interface{}{"foo": "bar", "abc": "123"}},
expected: []interface{}{map[string]interface{}{"foo": "bar", "abc": "123"}},
},
{
name: "slice with map, key pass, value string",
input: []interface{}{map[string]interface{}{"foo": "bar", "pass": "123"}},
expected: []interface{}{map[string]interface{}{"foo": "bar", "pass": "REDACTED"}},
},
{
name: "slice with nested slice, no pass",
input: []interface{}{[]interface{}{"a", "b", "c"}},
expected: []interface{}{[]interface{}{"a", "b", "c"}},
},
{
name: "slice with nested slice, with password",
input: []interface{}{[]interface{}{map[string]interface{}{"pass": "foo"}}},
expected: []interface{}{[]interface{}{map[string]interface{}{"pass": "REDACTED"}}},
},
{
name: "slice with empty map",
input: []interface{}{map[string]interface{}{}},
expected: []interface{}{map[string]interface{}{}},
},
{
name: "slice with empty slice",
input: []interface{}{[]interface{}{}},
expected: []interface{}{[]interface{}{}},
},
{
name: "slice with nil map",
input: []interface{}{nilMap},
expected: []interface{}{nilMap},
},
{
name: "slice with nil slice",
input: []interface{}{nilSlice},
expected: []interface{}{nilSlice},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
redacted := RedactPasswords(test.input)
assert.Equal(t, test.expected, redacted)
})
}
}

0 comments on commit 7d1bf99

Please sign in to comment.