Skip to content

Commit

Permalink
Fix secret sanitizing for non string values (#113)
Browse files Browse the repository at this point in the history
- Use callback instead of fixed value for secret replacement.
- Find all strings recursively in JSON objects for replacement.
- Handle difference between gRPC using "value" and state using a
"plaintext" string.

Fixes #108
  • Loading branch information
danielrbradley authored Oct 23, 2024
1 parent e07c9c9 commit 1c91af6
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 18 deletions.
71 changes: 57 additions & 14 deletions pulumitest/sanitize/sanitize.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,44 +51,87 @@ func SanitizeSecretsInGrpcLog(log json.RawMessage) json.RawMessage {
return log
}

sanitized := sanitizeSecretsInObject(data, map[string]any{
secretSignature: "1b47061264138c4ac30d75fd1eb44270",
"value": plaintextSub,
})
sanitized := sanitizeSecretsInObject(data, sanitizeGrpcSecret)
sanitizedBytes, err := json.Marshal(sanitized)
if err != nil {
return log
}
return sanitizedBytes
}

func sanitizeGrpcSecret(secretObj map[string]any) map[string]any {
// gRPC logs contain a field called "value" which is any JSON value
secretObj["value"] = sanitizeStringsRecursively(secretObj["value"])
return secretObj
}

func sanitizeSecretsInResources(resources []apitype.ResourceV3) {
for i, r := range resources {
r.Inputs = sanitizeSecretsInObject(r.Inputs, stateSecretReplacement)
r.Outputs = sanitizeSecretsInObject(r.Outputs, stateSecretReplacement)
r.Inputs = sanitizeSecretsInObject(r.Inputs, sanitizeStateSecret)
r.Outputs = sanitizeSecretsInObject(r.Outputs, sanitizeStateSecret)
resources[i] = r
}
}

var stateSecretReplacement = map[string]any{
secretSignature: "1b47061264138c4ac30d75fd1eb44270",
"plaintext": `"` + plaintextSub + `"`, // must be valid JSON, hence quoted
func sanitizeStateSecret(secretObj map[string]any) map[string]any {
// State file secrets either have a plaintext field which is any serialized JSON value
// of a cyphertext field which is and encrypted version of the JSON value.
plaintext, hasPlaintext := secretObj["plaintext"]
if !hasPlaintext {
return secretObj
}
plaintextString, isString := plaintext.(string)
if !isString {
return secretObj
}
var jsonValue any
err := json.Unmarshal([]byte(plaintextString), &jsonValue)
if err != nil {
return secretObj
}
sanitized := sanitizeStringsRecursively(jsonValue)
sanitizedBytes, err := json.Marshal(sanitized)
if err != nil {
return secretObj
}
secretObj["plaintext"] = string(sanitizedBytes)
return secretObj
}

func sanitizeSecretsInObject(obj map[string]any, secretReplacement map[string]any) map[string]any {
func sanitizeSecretsInObject(obj map[string]any, sanitizeSecret func(map[string]any) map[string]any) map[string]any {
copy := map[string]any{}
for k, v := range obj {
innerObj, ok := v.(map[string]any)
if ok {
_, hasSecret := innerObj[secretSignature]
if hasSecret {
copy[k] = secretReplacement
_, hasSecretSignature := innerObj[secretSignature]
if hasSecretSignature {
copy[k] = sanitizeSecret(innerObj)
} else {
copy[k] = sanitizeSecretsInObject(innerObj, secretReplacement)
copy[k] = sanitizeSecretsInObject(innerObj, sanitizeSecret)
}
} else {
copy[k] = v
}
}
return copy
}

func sanitizeStringsRecursively(value any) any {
switch typedValue := value.(type) {
case string:
return plaintextSub
case []any:
sanitizedSlice := make([]any, len(typedValue))
for i, v := range typedValue {
sanitizedSlice[i] = sanitizeStringsRecursively(v)
}
return sanitizedSlice
case map[string]any:
sanitizedMap := make(map[string]any, len(typedValue))
for k, v := range typedValue {
sanitizedMap[k] = sanitizeStringsRecursively(v)
}
return sanitizedMap
}
return value
}
20 changes: 16 additions & 4 deletions pulumitest/sanitize/sanitize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,16 @@ func TestSanitizeSecretsInObject(t *testing.T) {
}

expected := map[string]any{
"secondaryAccessKey": stateSecretReplacement,
"secondaryAccessKey": map[string]any{
secretSignature: "1b47061264138c4ac30d75fd1eb44270",
"plaintext": "sanitized",
},
}

assert.Equal(t, expected, sanitizeSecretsInObject(input, stateSecretReplacement))
assert.Equal(t, expected, sanitizeSecretsInObject(input, func(m map[string]any) map[string]any {
m["plaintext"] = "sanitized"
return m
}))
})

t.Run("nested", func(t *testing.T) {
Expand All @@ -59,12 +65,18 @@ func TestSanitizeSecretsInObject(t *testing.T) {
"bar": 1,
"foo": map[string]any{
"inner": map[string]any{
"secondaryAccessKey": stateSecretReplacement,
"secondaryAccessKey": map[string]any{
secretSignature: "1b47061264138c4ac30d75fd1eb44270",
"plaintext": "sanitized",
},
},
},
}

assert.Equal(t, expected, sanitizeSecretsInObject(input, stateSecretReplacement))
assert.Equal(t, expected, sanitizeSecretsInObject(input, func(m map[string]any) map[string]any {
m["plaintext"] = "sanitized"
return m
}))
})
}

Expand Down

0 comments on commit 1c91af6

Please sign in to comment.