Skip to content

Commit 70b0331

Browse files
committed
sanitize: allow for the sanitization of sensitive values
This adds a new package and functions for sanitization of values marked as sensitive in the plan, where we can get particular data to do it. This data is derived in a number of ways, also documented in the top-level SanitizePlan function: * ResourceChanges are sanitized based on BeforeSensitive and AfterSensitive fields. * Variables are sanitized based on variable config data found in the root module of the Config. * PlannedValues are sanitized based on the values found in AfterSensitive in ResourceChanges. Outputs are sanitized according to the appropriate sensitivity flags provided for the output. * PriorState is sanitized based on the values found in BeforeSensitive in ResourceChanges. Outputs are sanitized according to the appropriate sensitivity flags provided for the output. * OutputChanges are sanitized based on the values found in BeforeSensitive and AfterSensitive. This generally means that any sensitive output will have OutputChange fully obfuscated as the BeforeSensitive and AfterSensitive in outputs are opaquely the same.
1 parent dd1a819 commit 70b0331

14 files changed

+1757
-0
lines changed

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,7 @@ go 1.13
55
require (
66
github.com/davecgh/go-spew v1.1.1
77
github.com/google/go-cmp v0.3.1
8+
github.com/mitchellh/copystructure v1.2.0
9+
github.com/sebdah/goldie v1.0.0
810
github.com/zclconf/go-cty v1.2.1
911
)

go.sum

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
2+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
23
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
34
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
45
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -8,6 +9,17 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
89
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
910
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
1011
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
12+
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
13+
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
14+
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
15+
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
16+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
17+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
18+
github.com/sebdah/goldie v1.0.0 h1:9GNhIat69MSlz/ndaBg48vl9dF5fI+NBB6kfOxgfkMc=
19+
github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4=
20+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
21+
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
22+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
1123
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
1224
github.com/zclconf/go-cty v1.2.1 h1:vGMsygfmeCl4Xb6OA5U5XVAaQZ69FvoG7X2jUtQujb8=
1325
github.com/zclconf/go-cty v1.2.1/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=

sanitize/copy.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package sanitize
2+
3+
import (
4+
"reflect"
5+
6+
tfjson "github.com/hashicorp/terraform-json"
7+
"github.com/mitchellh/copystructure"
8+
)
9+
10+
// copyStructureCopy is an internal function that wraps copystructure.Copy with
11+
// a shallow copier for unknown values.
12+
//
13+
// Performing the shallow copy of the unknown values is important
14+
// here, as unknown values are parsed in with the main terraform-json
15+
// package as singletons, and must continue to be comparable.
16+
func copyStructureCopy(v interface{}) (interface{}, error) {
17+
c := &copystructure.Config{
18+
ShallowCopiers: map[reflect.Type]struct{}{
19+
reflect.TypeOf(tfjson.UnknownConstantValue): struct{}{},
20+
},
21+
}
22+
23+
return c.Copy(v)
24+
}
25+
26+
// copyChange copies a Change value and returns the copy.
27+
func copyChange(old *tfjson.Change) (*tfjson.Change, error) {
28+
c, err := copyStructureCopy(old)
29+
if err != nil {
30+
return nil, err
31+
}
32+
33+
return c.(*tfjson.Change), nil
34+
}
35+
36+
// copyPlan copies a Plan value and returns the copy.
37+
func copyPlan(old *tfjson.Plan) (*tfjson.Plan, error) {
38+
c, err := copyStructureCopy(old)
39+
if err != nil {
40+
return nil, err
41+
}
42+
43+
return c.(*tfjson.Plan), nil
44+
}
45+
46+
// copyPlanVariable copies a PlanVariable value and returns the copy.
47+
func copyPlanVariable(old *tfjson.PlanVariable) (*tfjson.PlanVariable, error) {
48+
c, err := copyStructureCopy(old)
49+
if err != nil {
50+
return nil, err
51+
}
52+
53+
return c.(*tfjson.PlanVariable), nil
54+
}
55+
56+
// copyStateResource copies a StateResource value and returns the copy.
57+
func copyStateResource(old *tfjson.StateResource) (*tfjson.StateResource, error) {
58+
c, err := copyStructureCopy(old)
59+
if err != nil {
60+
return nil, err
61+
}
62+
63+
return c.(*tfjson.StateResource), nil
64+
}
65+
66+
// copyStateOutput copies a StateOutput value and returns the copy.
67+
func copyStateOutputs(old map[string]*tfjson.StateOutput) (map[string]*tfjson.StateOutput, error) {
68+
c, err := copystructure.Copy(old)
69+
if err != nil {
70+
return nil, err
71+
}
72+
73+
return c.(map[string]*tfjson.StateOutput), nil
74+
}

sanitize/copy_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package sanitize
2+
3+
import (
4+
"testing"
5+
6+
tfjson "github.com/hashicorp/terraform-json"
7+
)
8+
9+
func TestCopyStructureCopy(t *testing.T) {
10+
in := tfjson.UnknownConstantValue
11+
out, err := copyStructureCopy(in)
12+
if err != nil {
13+
t.Fatal(err)
14+
}
15+
16+
if in != out {
17+
t.Fatal("did not shallow copy")
18+
}
19+
}

sanitize/sanitize_change.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package sanitize
2+
3+
import (
4+
tfjson "github.com/hashicorp/terraform-json"
5+
)
6+
7+
// SanitizeChange traverses a Change and replaces all values at
8+
// the particular locations marked by BeforeSensitive AfterSensitive
9+
// with the value supplied as replaceWith.
10+
//
11+
// A new change is issued.
12+
func SanitizeChange(old *tfjson.Change, replaceWith interface{}) (*tfjson.Change, error) {
13+
result, err := copyChange(old)
14+
if err != nil {
15+
return nil, err
16+
}
17+
18+
result.Before = sanitizeChangeValue(result.Before, result.BeforeSensitive, replaceWith)
19+
result.After = sanitizeChangeValue(result.After, result.AfterSensitive, replaceWith)
20+
21+
return result, nil
22+
}
23+
24+
func sanitizeChangeValue(old, sensitive, replaceWith interface{}) interface{} {
25+
// Only expect deep types that we would normally see in JSON, so
26+
// arrays and objects.
27+
switch x := old.(type) {
28+
case []interface{}:
29+
if filterSlice, ok := sensitive.([]interface{}); ok {
30+
for i := range filterSlice {
31+
if i >= len(x) {
32+
break
33+
}
34+
35+
x[i] = sanitizeChangeValue(x[i], filterSlice[i], replaceWith)
36+
}
37+
}
38+
case map[string]interface{}:
39+
if filterMap, ok := sensitive.(map[string]interface{}); ok {
40+
for filterKey := range filterMap {
41+
if value, ok := x[filterKey]; ok {
42+
x[filterKey] = sanitizeChangeValue(value, filterMap[filterKey], replaceWith)
43+
}
44+
}
45+
}
46+
}
47+
48+
if shouldFilter, ok := sensitive.(bool); ok && shouldFilter {
49+
return replaceWith
50+
}
51+
52+
return old
53+
}

sanitize/sanitize_change_test.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package sanitize
2+
3+
import (
4+
"testing"
5+
6+
"github.com/google/go-cmp/cmp"
7+
tfjson "github.com/hashicorp/terraform-json"
8+
)
9+
10+
type testChangeCase struct {
11+
name string
12+
old *tfjson.Change
13+
expected *tfjson.Change
14+
}
15+
16+
func changeCases() []testChangeCase {
17+
return []testChangeCase{
18+
{
19+
name: "basic",
20+
old: &tfjson.Change{
21+
Before: map[string]interface{}{
22+
"foo": map[string]interface{}{"a": "foo"},
23+
"bar": map[string]interface{}{"a": "foo"},
24+
"baz": map[string]interface{}{"a": "foo"},
25+
"qux": map[string]interface{}{
26+
"a": map[string]interface{}{
27+
"b": "foo",
28+
},
29+
"c": "bar",
30+
},
31+
"quxx": map[string]interface{}{
32+
"a": map[string]interface{}{
33+
"b": "foo",
34+
},
35+
"c": "bar",
36+
},
37+
},
38+
After: map[string]interface{}{
39+
"one": map[string]interface{}{"x": "one"},
40+
"two": map[string]interface{}{"x": "one"},
41+
"three": map[string]interface{}{"x": "one"},
42+
"four": map[string]interface{}{
43+
"x": map[string]interface{}{
44+
"y": "one",
45+
},
46+
"z": "two",
47+
},
48+
"five": map[string]interface{}{
49+
"x": map[string]interface{}{
50+
"y": "one",
51+
},
52+
"z": "two",
53+
},
54+
},
55+
BeforeSensitive: map[string]interface{}{
56+
"foo": map[string]interface{}{},
57+
"bar": true,
58+
"baz": map[string]interface{}{"a": true},
59+
"qux": map[string]interface{}{},
60+
"quxx": map[string]interface{}{"c": true},
61+
},
62+
AfterSensitive: map[string]interface{}{
63+
"one": map[string]interface{}{},
64+
"two": true,
65+
"three": map[string]interface{}{"x": true},
66+
"four": map[string]interface{}{},
67+
"five": map[string]interface{}{"z": true},
68+
},
69+
},
70+
expected: &tfjson.Change{
71+
Before: map[string]interface{}{
72+
"foo": map[string]interface{}{"a": "foo"},
73+
"bar": DefaultSensitiveValue,
74+
"baz": map[string]interface{}{"a": DefaultSensitiveValue},
75+
"qux": map[string]interface{}{
76+
"a": map[string]interface{}{
77+
"b": "foo",
78+
},
79+
"c": "bar",
80+
},
81+
"quxx": map[string]interface{}{
82+
"a": map[string]interface{}{
83+
"b": "foo",
84+
},
85+
"c": DefaultSensitiveValue,
86+
},
87+
},
88+
After: map[string]interface{}{
89+
"one": map[string]interface{}{"x": "one"},
90+
"two": DefaultSensitiveValue,
91+
"three": map[string]interface{}{"x": DefaultSensitiveValue},
92+
"four": map[string]interface{}{
93+
"x": map[string]interface{}{
94+
"y": "one",
95+
},
96+
"z": "two",
97+
},
98+
"five": map[string]interface{}{
99+
"x": map[string]interface{}{
100+
"y": "one",
101+
},
102+
"z": DefaultSensitiveValue,
103+
},
104+
},
105+
BeforeSensitive: map[string]interface{}{
106+
"foo": map[string]interface{}{},
107+
"bar": true,
108+
"baz": map[string]interface{}{"a": true},
109+
"qux": map[string]interface{}{},
110+
"quxx": map[string]interface{}{"c": true},
111+
},
112+
AfterSensitive: map[string]interface{}{
113+
"one": map[string]interface{}{},
114+
"two": true,
115+
"three": map[string]interface{}{"x": true},
116+
"four": map[string]interface{}{},
117+
"five": map[string]interface{}{"z": true},
118+
},
119+
},
120+
},
121+
}
122+
}
123+
124+
func TestSanitizeChange(t *testing.T) {
125+
for i, tc := range changeCases() {
126+
tc := tc
127+
t.Run(tc.name, func(t *testing.T) {
128+
actual, err := SanitizeChange(tc.old, DefaultSensitiveValue)
129+
if err != nil {
130+
t.Fatal(err)
131+
}
132+
133+
if diff := cmp.Diff(tc.expected, actual); diff != "" {
134+
t.Errorf("SanitizeChange() mismatch (-expected +actual):\n%s", diff)
135+
}
136+
137+
if diff := cmp.Diff(changeCases()[i].old, tc.old); diff != "" {
138+
t.Errorf("SanitizeChange() altered original (-expected +actual):\n%s", diff)
139+
}
140+
})
141+
}
142+
}

0 commit comments

Comments
 (0)