Skip to content

Commit

Permalink
bugtool: Add json masking function
Browse files Browse the repository at this point in the history
[upstream commit 568dbc5]

This commit is to add a generic json field masking based on the field
name.

[ Quentin: Replaced the use of slices.Contains(), unsupported with the
  Go version used with v1.13. ]

Signed-off-by: Tam Mach <tam.mach@cilium.io>
Signed-off-by: Quentin Monnet <qmo@qmon.net>
  • Loading branch information
sayboras authored and michi-covalent committed Jun 8, 2024
1 parent dc19b3d commit 958d7b7
Show file tree
Hide file tree
Showing 3 changed files with 258 additions and 0 deletions.
64 changes: 64 additions & 0 deletions bugtool/cmd/mask.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package cmd

import (
"encoding/json"
)

const (
redacted = "[redacted]"
ident = "\t"
)

// jsonFieldMaskPostProcess returns a postProcessFunc that masks the specified field names.
// The input byte slice is expected to be a JSON object.
func jsonFieldMaskPostProcess(fieldNames []string) postProcessFunc {
return func(b []byte) ([]byte, error) {
return maskFields(b, fieldNames)
}
}

func maskFields(b []byte, fieldNames []string) ([]byte, error) {
var data map[string]interface{}

if err := json.Unmarshal(b, &data); err != nil {
return nil, err
}

mask(data, fieldNames)

// MarshalIndent is used to make the output more readable.
return json.MarshalIndent(data, "", ident)
}

func contains(names []string, name string) bool {
for _, n := range names {
if n == name {
return true
}
}
return false
}

func mask(data map[string]interface{}, fieldNames []string) {
for k, v := range data {
if contains(fieldNames, k) {
data[k] = redacted
continue
}

switch t := v.(type) {
case map[string]interface{}:
mask(t, fieldNames)
case []interface{}:
for i, item := range t {
if subData, ok := item.(map[string]interface{}); ok {
mask(subData, fieldNames)
t[i] = subData
}
}
}
}
}
192 changes: 192 additions & 0 deletions bugtool/cmd/mask_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package cmd

import (
"testing"

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

func Test_jsonFieldMaskPostProcess(t *testing.T) {
type args struct {
input []byte
fieldNames []string
}
tests := []struct {
name string
args args
want []byte
wantErr bool
}{
{
name: "simple struct",
args: args{
input: []byte(`{
"username": "user1",
"password": "mypassword",
"email": "user1@example.com"
}`),
fieldNames: []string{"password"},
},
want: []byte(`{
"username": "user1",
"password": "[redacted]",
"email": "user1@example.com"
}`),
},
{
name: "array struct",
args: args{
input: []byte(`{
"username": "user1",
"secrets": [
{
"password": "mypassword"
},
{
"password": "anotherone"
}
],
"password": "mypassword",
"email": "user1@example.com"
}`),
fieldNames: []string{"password"},
},
want: []byte(`{
"username": "user1",
"secrets": [
{
"password": "[redacted]"
},
{
"password": "[redacted]"
}
],
"password": "[redacted]",
"email": "user1@example.com"
}`),
},
{
name: "nested struct",
args: args{
input: []byte(`{
"username": "user1",
"password": "mypassword",
"email": "user1@example.com",
"complex": {
"password": "anotherpassword"
}
}`),
fieldNames: []string{"password"},
},
want: []byte(`{
"username": "user1",
"password": "[redacted]",
"email": "user1@example.com",
"complex": {
"password": "[redacted]"
}
}`),
},
{
name: "nested array struct",
args: args{
input: []byte(`{
"username": "user1",
"password": "mypassword",
"email": "user1@example.com",
"complex": {
"secrets": [
{
"password": "mypassword"
},
{
"password": "anotherpassword"
}
]
}
}`),
fieldNames: []string{"password"},
},
want: []byte(`{
"username": "user1",
"password": "[redacted]",
"email": "user1@example.com",
"complex": {
"secrets": [
{
"password": "[redacted]"
},
{
"password": "[redacted]"
}
]
}
}`),
},
{
name: "no masked field",
args: args{
input: []byte(`{
"username": "user1",
"password": "mypassword",
"email": "user1@example.com",
"complex": {
"password": "anotherpassword"
}
}`),
fieldNames: []string{"no-such-field"},
},
want: []byte(`{
"username": "user1",
"password": "mypassword",
"email": "user1@example.com",
"complex": {
"password": "anotherpassword"
}
}`),
},
{
name: "mask object field",
args: args{
input: []byte(`{
"username": "user1",
"password": "mypassword",
"email": "user1@example.com",
"complex": {
"password": "anotherpassword"
}
}`),
fieldNames: []string{"complex"},
},
want: []byte(`{
"username": "user1",
"password": "mypassword",
"email": "user1@example.com",
"complex": "[redacted]"
}`),
},
{
name: "invalid input",
args: args{
input: []byte(`{"username": "user1",}`),
fieldNames: []string{"password"},
},
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := jsonFieldMaskPostProcess(tt.args.fieldNames)(tt.args.input)
require.Equal(t, tt.wantErr, err != nil)
// only assert the output if there is no error
// as JSONEq func is used to compare the output
if !tt.wantErr {
require.JSONEq(t, string(tt.want), string(got))
}
})
}
}
2 changes: 2 additions & 0 deletions bugtool/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ func isValidArchiveType(archiveType string) bool {
return false
}

type postProcessFunc func(output []byte) ([]byte, error)

func runTool() {
// Validate archive type
if !isValidArchiveType(archiveType) {
Expand Down

0 comments on commit 958d7b7

Please sign in to comment.