diff --git a/go.mod b/go.mod index 699948a90c..dbc341b1c0 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/aws/aws-lambda-go v1.17.0 github.com/aws/aws-sdk-go v1.32.7 github.com/cenkalti/backoff/v4 v4.0.2 + github.com/fatih/structtag v1.2.0 github.com/go-openapi/errors v0.19.6 github.com/go-openapi/runtime v0.19.19 github.com/go-openapi/strfmt v0.19.5 @@ -20,6 +21,7 @@ require ( github.com/kelseyhightower/envconfig v1.4.0 github.com/leodido/go-urn v1.2.0 // indirect github.com/magefile/mage v1.9.0 + github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.6.1 github.com/tidwall/gjson v1.6.0 diff --git a/go.sum b/go.sum index 2ee06ff517..10242c52a6 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= diff --git a/internal/log_analysis/log_processor/pantherlog/omitempty/omitempty.go b/internal/log_analysis/log_processor/pantherlog/omitempty/omitempty.go new file mode 100644 index 0000000000..97816050de --- /dev/null +++ b/internal/log_analysis/log_processor/pantherlog/omitempty/omitempty.go @@ -0,0 +1,95 @@ +package omitempty + +/** + * Panther is a Cloud-Native SIEM for the Modern Security Team. + * Copyright (C) 2020 Panther Labs Inc + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import ( + "reflect" + + "github.com/fatih/structtag" + jsoniter "github.com/json-iterator/go" + "github.com/modern-go/reflect2" +) + +// New injects omitempty option to all fields +func New(key string) jsoniter.Extension { + if key == "" { + key = "json" + } + return &omitemptyExt{ + key: key, + } +} + +type omitemptyExt struct { + jsoniter.DummyExtension + key string +} + +func (ext *omitemptyExt) UpdateStructDescriptor(desc *jsoniter.StructDescriptor) { + for _, binding := range desc.Fields { + field := binding.Field + // Assert that the struct descriptor does not contain anonymous fields (jsoniter omits them) + if field.Anonymous() { + panic("Anonymous field in struct descriptor") + } + tag := injectOmitempty(field.Tag(), ext.key) + binding.Field = InjectTag(field, tag) + } +} + +func InjectTag(field reflect2.StructField, tag reflect.StructTag) reflect2.StructField { + return &fieldExt{ + StructField: field, + tag: tag, + } +} + +type fieldExt struct { + reflect2.StructField + tag reflect.StructTag +} + +func (ext *fieldExt) Tag() reflect.StructTag { + if ext.tag != "" { + return ext.tag + } + return ext.StructField.Tag() +} + +func injectOmitempty(original reflect.StructTag, key string) reflect.StructTag { + tags, err := structtag.Parse(string(original)) + if err != nil { + return original + } + tag, err := tags.Get(key) + if err != nil { + tag := structtag.Tag{ + Key: key, + Options: []string{"omitempty"}, + } + _ = tags.Set(&tag) + return reflect.StructTag(tags.String()) + } + // Assert jsoniter omits fields witg `-` name + if tag.Name == "-" { + panic("JSON-omittted field in struct descriptor") + } + tags.AddOptions(key, "omitempty") + return reflect.StructTag(tags.String()) +} diff --git a/internal/log_analysis/log_processor/pantherlog/omitempty/omitempty_test.go b/internal/log_analysis/log_processor/pantherlog/omitempty/omitempty_test.go new file mode 100644 index 0000000000..cfb5c64e02 --- /dev/null +++ b/internal/log_analysis/log_processor/pantherlog/omitempty/omitempty_test.go @@ -0,0 +1,70 @@ +package omitempty + +/** + * Panther is a Cloud-Native SIEM for the Modern Security Team. + * Copyright (C) 2020 Panther Labs Inc + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import ( + "testing" + + jsoniter "github.com/json-iterator/go" + "github.com/stretchr/testify/require" +) + +func TestNew(t *testing.T) { + api := jsoniter.Config{}.Froze() + api.RegisterExtension(New("json")) + + type A struct { + Foo string `json:"foo"` + Bar string `json:",omitempty"` + Baz string + Nested *A `json:"nested"` + } + type B struct { + Qux string `json:"qux"` + A + Ignore string `json:"-"` + } + { + out, err := api.MarshalToString(&A{ + Nested: &A{}, + }) + require.NoError(t, err) + require.Equal(t, `{"nested":{}}`, out) + } + { + out, err := api.MarshalToString(&A{}) + require.NoError(t, err) + require.Equal(t, `{}`, out) + } + { + out, err := api.MarshalToString(&B{}) + require.NoError(t, err) + require.Equal(t, `{}`, out) + } + { + out, err := api.MarshalToString(&B{ + Qux: "qux", + A: A{ + Foo: "foo", + }, + }) + require.NoError(t, err) + require.Equal(t, `{"qux":"qux","foo":"foo"}`, out) + } +}