Skip to content

Commit 958d7b7

Browse files
sayborasmichi-covalent
authored andcommitted
bugtool: Add json masking function
[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>
1 parent dc19b3d commit 958d7b7

File tree

3 files changed

+258
-0
lines changed

3 files changed

+258
-0
lines changed

Diff for: bugtool/cmd/mask.go

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright Authors of Cilium
3+
4+
package cmd
5+
6+
import (
7+
"encoding/json"
8+
)
9+
10+
const (
11+
redacted = "[redacted]"
12+
ident = "\t"
13+
)
14+
15+
// jsonFieldMaskPostProcess returns a postProcessFunc that masks the specified field names.
16+
// The input byte slice is expected to be a JSON object.
17+
func jsonFieldMaskPostProcess(fieldNames []string) postProcessFunc {
18+
return func(b []byte) ([]byte, error) {
19+
return maskFields(b, fieldNames)
20+
}
21+
}
22+
23+
func maskFields(b []byte, fieldNames []string) ([]byte, error) {
24+
var data map[string]interface{}
25+
26+
if err := json.Unmarshal(b, &data); err != nil {
27+
return nil, err
28+
}
29+
30+
mask(data, fieldNames)
31+
32+
// MarshalIndent is used to make the output more readable.
33+
return json.MarshalIndent(data, "", ident)
34+
}
35+
36+
func contains(names []string, name string) bool {
37+
for _, n := range names {
38+
if n == name {
39+
return true
40+
}
41+
}
42+
return false
43+
}
44+
45+
func mask(data map[string]interface{}, fieldNames []string) {
46+
for k, v := range data {
47+
if contains(fieldNames, k) {
48+
data[k] = redacted
49+
continue
50+
}
51+
52+
switch t := v.(type) {
53+
case map[string]interface{}:
54+
mask(t, fieldNames)
55+
case []interface{}:
56+
for i, item := range t {
57+
if subData, ok := item.(map[string]interface{}); ok {
58+
mask(subData, fieldNames)
59+
t[i] = subData
60+
}
61+
}
62+
}
63+
}
64+
}

Diff for: bugtool/cmd/mask_test.go

+192
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright Authors of Cilium
3+
4+
package cmd
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func Test_jsonFieldMaskPostProcess(t *testing.T) {
13+
type args struct {
14+
input []byte
15+
fieldNames []string
16+
}
17+
tests := []struct {
18+
name string
19+
args args
20+
want []byte
21+
wantErr bool
22+
}{
23+
{
24+
name: "simple struct",
25+
args: args{
26+
input: []byte(`{
27+
"username": "user1",
28+
"password": "mypassword",
29+
"email": "user1@example.com"
30+
}`),
31+
fieldNames: []string{"password"},
32+
},
33+
want: []byte(`{
34+
"username": "user1",
35+
"password": "[redacted]",
36+
"email": "user1@example.com"
37+
}`),
38+
},
39+
{
40+
name: "array struct",
41+
args: args{
42+
input: []byte(`{
43+
"username": "user1",
44+
"secrets": [
45+
{
46+
"password": "mypassword"
47+
},
48+
{
49+
"password": "anotherone"
50+
}
51+
],
52+
"password": "mypassword",
53+
"email": "user1@example.com"
54+
}`),
55+
fieldNames: []string{"password"},
56+
},
57+
want: []byte(`{
58+
"username": "user1",
59+
"secrets": [
60+
{
61+
"password": "[redacted]"
62+
},
63+
{
64+
"password": "[redacted]"
65+
}
66+
],
67+
"password": "[redacted]",
68+
"email": "user1@example.com"
69+
}`),
70+
},
71+
{
72+
name: "nested struct",
73+
args: args{
74+
input: []byte(`{
75+
"username": "user1",
76+
"password": "mypassword",
77+
"email": "user1@example.com",
78+
"complex": {
79+
"password": "anotherpassword"
80+
}
81+
}`),
82+
fieldNames: []string{"password"},
83+
},
84+
want: []byte(`{
85+
"username": "user1",
86+
"password": "[redacted]",
87+
"email": "user1@example.com",
88+
"complex": {
89+
"password": "[redacted]"
90+
}
91+
}`),
92+
},
93+
{
94+
name: "nested array struct",
95+
args: args{
96+
input: []byte(`{
97+
"username": "user1",
98+
"password": "mypassword",
99+
"email": "user1@example.com",
100+
"complex": {
101+
"secrets": [
102+
{
103+
"password": "mypassword"
104+
},
105+
{
106+
"password": "anotherpassword"
107+
}
108+
]
109+
}
110+
}`),
111+
fieldNames: []string{"password"},
112+
},
113+
want: []byte(`{
114+
"username": "user1",
115+
"password": "[redacted]",
116+
"email": "user1@example.com",
117+
"complex": {
118+
"secrets": [
119+
{
120+
"password": "[redacted]"
121+
},
122+
{
123+
"password": "[redacted]"
124+
}
125+
]
126+
}
127+
}`),
128+
},
129+
{
130+
name: "no masked field",
131+
args: args{
132+
input: []byte(`{
133+
"username": "user1",
134+
"password": "mypassword",
135+
"email": "user1@example.com",
136+
"complex": {
137+
"password": "anotherpassword"
138+
}
139+
}`),
140+
fieldNames: []string{"no-such-field"},
141+
},
142+
want: []byte(`{
143+
"username": "user1",
144+
"password": "mypassword",
145+
"email": "user1@example.com",
146+
"complex": {
147+
"password": "anotherpassword"
148+
}
149+
}`),
150+
},
151+
{
152+
name: "mask object field",
153+
args: args{
154+
input: []byte(`{
155+
"username": "user1",
156+
"password": "mypassword",
157+
"email": "user1@example.com",
158+
"complex": {
159+
"password": "anotherpassword"
160+
}
161+
}`),
162+
fieldNames: []string{"complex"},
163+
},
164+
want: []byte(`{
165+
"username": "user1",
166+
"password": "mypassword",
167+
"email": "user1@example.com",
168+
"complex": "[redacted]"
169+
}`),
170+
},
171+
{
172+
name: "invalid input",
173+
args: args{
174+
input: []byte(`{"username": "user1",}`),
175+
fieldNames: []string{"password"},
176+
},
177+
want: nil,
178+
wantErr: true,
179+
},
180+
}
181+
for _, tt := range tests {
182+
t.Run(tt.name, func(t *testing.T) {
183+
got, err := jsonFieldMaskPostProcess(tt.args.fieldNames)(tt.args.input)
184+
require.Equal(t, tt.wantErr, err != nil)
185+
// only assert the output if there is no error
186+
// as JSONEq func is used to compare the output
187+
if !tt.wantErr {
188+
require.JSONEq(t, string(tt.want), string(got))
189+
}
190+
})
191+
}
192+
}

Diff for: bugtool/cmd/root.go

+2
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ func isValidArchiveType(archiveType string) bool {
172172
return false
173173
}
174174

175+
type postProcessFunc func(output []byte) ([]byte, error)
176+
175177
func runTool() {
176178
// Validate archive type
177179
if !isValidArchiveType(archiveType) {

0 commit comments

Comments
 (0)