Skip to content

Commit c64ba8d

Browse files
authored
Validate expected fields normalization (#765)
ECS fields can include normalization rules, indicated by the `normalize` parameter. By now only the `array` rule exist, that indicates that values should be included in the field as an array, even if there is a single element. Validate this on test results for packages with `format_version` >= 2.0.0.
1 parent 20e8351 commit c64ba8d

36 files changed

+1377
-1062
lines changed

internal/common/helpers.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,17 @@ func StringSliceContains(slice []string, s string) bool {
2222
}
2323
return false
2424
}
25+
26+
// StringSlicesUnion joins multiple slices and returns an slice with the distinct
27+
// elements of all of them.
28+
func StringSlicesUnion(slices ...[]string) (result []string) {
29+
for _, slice := range slices {
30+
for _, elem := range slice {
31+
if StringSliceContains(result, elem) {
32+
continue
33+
}
34+
result = append(result, elem)
35+
}
36+
}
37+
return
38+
}

internal/common/helpers_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,22 @@ func TestStringSliceContains(t *testing.T) {
3939
assert.Equalf(t, c.expected, found, "checking if slice %v contains '%s'", c.slice, c.s)
4040
}
4141
}
42+
43+
func TestStringSlicesUnion(t *testing.T) {
44+
cases := []struct {
45+
slices [][]string
46+
expected []string
47+
}{
48+
{nil, nil},
49+
{[][]string{{"foo", "bar"}, nil}, []string{"foo", "bar"}},
50+
{[][]string{nil, {"foo", "bar"}}, []string{"foo", "bar"}},
51+
{[][]string{{"foo", "bar"}, {"foo", "bar"}}, []string{"foo", "bar"}},
52+
{[][]string{{"foo", "baz"}, {"foo", "bar"}}, []string{"foo", "bar", "baz"}},
53+
{[][]string{{"foo", "bar"}, {"foo", "baz"}}, []string{"foo", "bar", "baz"}},
54+
}
55+
56+
for _, c := range cases {
57+
result := StringSlicesUnion(c.slices...)
58+
assert.ElementsMatch(t, c.expected, result)
59+
}
60+
}

internal/configuration/locations/locations.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ var (
4040
dockerCustomAgentDeployerDir = filepath.Join(deployerDir, "docker_custom_agent")
4141
)
4242

43-
//LocationManager maintains an instance of a config path location
43+
// LocationManager maintains an instance of a config path location
4444
type LocationManager struct {
4545
stackPath string
4646
}

internal/fields/dependency_manager.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,10 @@ func transformImportedField(fd FieldDefinition) common.MapStr {
262262
m["doc_values"] = *fd.DocValues
263263
}
264264

265+
if len(fd.Normalize) > 0 {
266+
m["normalize"] = fd.Normalize
267+
}
268+
265269
if len(fd.MultiFields) > 0 {
266270
var t []common.MapStr
267271
for _, f := range fd.MultiFields {

internal/fields/dependency_manager_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,51 @@ func TestDependencyManagerInjectExternalFields(t *testing.T) {
200200
changed: true,
201201
valid: true,
202202
},
203+
{
204+
title: "array field",
205+
defs: []common.MapStr{
206+
{
207+
"name": "host.ip",
208+
"external": "test",
209+
},
210+
},
211+
result: []common.MapStr{
212+
{
213+
"name": "host.ip",
214+
"type": "ip",
215+
"description": "Host ip addresses.",
216+
"normalize": []string{
217+
"array",
218+
},
219+
},
220+
},
221+
changed: true,
222+
valid: true,
223+
},
224+
{
225+
title: "array field override",
226+
defs: []common.MapStr{
227+
{
228+
"name": "container.id",
229+
"external": "test",
230+
"normalize": []string{
231+
"array",
232+
},
233+
},
234+
},
235+
result: []common.MapStr{
236+
{
237+
"name": "container.id",
238+
"type": "keyword",
239+
"description": "Container identifier.",
240+
"normalize": []string{
241+
"array",
242+
},
243+
},
244+
},
245+
changed: true,
246+
valid: true,
247+
},
203248
{
204249
title: "unknown field",
205250
defs: []common.MapStr{
@@ -335,6 +380,14 @@ func TestDependencyManagerInjectExternalFields(t *testing.T) {
335380
Index: &indexFalse,
336381
DocValues: &indexFalse,
337382
},
383+
{
384+
Name: "host.ip",
385+
Description: "Host ip addresses.",
386+
Type: "ip",
387+
Normalize: []string{
388+
"array",
389+
},
390+
},
338391
{
339392
Name: "source.mac",
340393
Description: "MAC address of the source.",

internal/fields/model.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type FieldDefinition struct {
2828
External string `yaml:"external"`
2929
Index *bool `yaml:"index"`
3030
DocValues *bool `yaml:"doc_values"`
31+
Normalize []string `yaml:"normalize,omitempty"`
3132
Fields FieldDefinitions `yaml:"fields,omitempty"`
3233
MultiFields []FieldDefinition `yaml:"multi_fields,omitempty"`
3334
}
@@ -73,6 +74,10 @@ func (orig *FieldDefinition) Update(fd FieldDefinition) {
7374
orig.DocValues = fd.DocValues
7475
}
7576

77+
if len(fd.Normalize) > 0 {
78+
orig.Normalize = common.StringSlicesUnion(orig.Normalize, fd.Normalize)
79+
}
80+
7681
if len(fd.Fields) > 0 {
7782
orig.Fields = updateFields(orig.Fields, fd.Fields)
7883
}

internal/fields/testdata/fields/fields.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,7 @@
1313
value: correct
1414
- name: ip_address
1515
type: ip
16+
- name: container.image.tag
17+
type: keyword
18+
normalize:
19+
- array
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"container.image.tag": "sometag"
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"container.image.tag": ["sometag"]
3+
}

internal/fields/validate.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"regexp"
1616
"strings"
1717

18+
"github.com/Masterminds/semver"
1819
"github.com/pkg/errors"
1920
"gopkg.in/yaml.v3"
2021

@@ -32,6 +33,9 @@ type Validator struct {
3233
// FieldDependencyManager resolves references to external fields
3334
FieldDependencyManager *DependencyManager
3435

36+
// SpecVersion contains the version of the spec used by the package.
37+
specVersion semver.Version
38+
3539
defaultNumericConversion bool
3640
numericKeywordFields map[string]struct{}
3741

@@ -44,6 +48,18 @@ type Validator struct {
4448
// ValidatorOption represents an optional flag that can be passed to CreateValidatorForDirectory.
4549
type ValidatorOption func(*Validator) error
4650

51+
// WithSpecVersion enables validation dependant of the spec version used by the package.
52+
func WithSpecVersion(version string) ValidatorOption {
53+
return func(v *Validator) error {
54+
sv, err := semver.NewVersion(version)
55+
if err != nil {
56+
return fmt.Errorf("invalid version %q: %v", version, err)
57+
}
58+
v.specVersion = *sv
59+
return nil
60+
}
61+
}
62+
4763
// WithDefaultNumericConversion configures the validator to accept defined keyword (or constant_keyword) fields as numeric-type.
4864
func WithDefaultNumericConversion() ValidatorOption {
4965
return func(v *Validator) error {
@@ -255,7 +271,12 @@ func (v *Validator) validateScalarElement(key string, val interface{}) error {
255271
val = fmt.Sprintf("%q", val)
256272
}
257273

258-
err := v.parseElementValue(key, *definition, val)
274+
err := v.validateExpectedNormalization(*definition, val)
275+
if err != nil {
276+
return errors.Wrapf(err, "field %q is not normalized as expected", key)
277+
}
278+
279+
err = v.parseElementValue(key, *definition, val)
259280
if err != nil {
260281
return errors.Wrap(err, "parsing field value failed")
261282
}
@@ -361,6 +382,22 @@ func compareKeys(key string, def FieldDefinition, searchedKey string) bool {
361382
return false
362383
}
363384

385+
func (v *Validator) validateExpectedNormalization(definition FieldDefinition, val interface{}) error {
386+
// Validate expected normalization starting with packages following spec v2 format.
387+
if v.specVersion.LessThan(semver.MustParse("2.0.0")) {
388+
return nil
389+
}
390+
for _, normalize := range definition.Normalize {
391+
switch normalize {
392+
case "array":
393+
if _, isArray := val.([]interface{}); val != nil && !isArray {
394+
return fmt.Errorf("expected array, found %q (%T)", val, val)
395+
}
396+
}
397+
}
398+
return nil
399+
}
400+
364401
// validSubField checks if the extra part that didn't match with any field definition,
365402
// matches with the possible sub field of complex fields like geo_point or histogram.
366403
func validSubField(def FieldDefinition, extraPart string) bool {

0 commit comments

Comments
 (0)