Skip to content

Commit 0191b1e

Browse files
sayborasjulianwiedmann
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. Signed-off-by: Tam Mach <tam.mach@cilium.io>
1 parent b648346 commit 0191b1e

File tree

3 files changed

+250
-0
lines changed

3 files changed

+250
-0
lines changed

Diff for: bugtool/cmd/mask.go

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

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
@@ -168,6 +168,8 @@ func isValidArchiveType(archiveType string) bool {
168168
return false
169169
}
170170

171+
type postProcessFunc func(output []byte) ([]byte, error)
172+
171173
func runTool() {
172174
// Validate archive type
173175
if !isValidArchiveType(archiveType) {

0 commit comments

Comments
 (0)