-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
port password redact feature from master to v3 branch
- Loading branch information
1 parent
69bdc7c
commit 7d1bf99
Showing
3 changed files
with
298 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
} | ||
} |