Skip to content

Commit 046a146

Browse files
authored
Validate that event.type is aligned with event.category (#961)
Fields like `event.category` in ECS define a list of allowed values for `event.type`, check that these values are aligned. This validation is only enabled for packages using at least format version 2.0.0.
1 parent c64ba8d commit 046a146

File tree

11 files changed

+322
-80
lines changed

11 files changed

+322
-80
lines changed

internal/fields/model.go

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ func cleanNested(parent *FieldDefinition) (base []FieldDefinition) {
205205
// AllowedValues is the list of allowed values for a field.
206206
type AllowedValues []AllowedValue
207207

208-
// Allowed returns true if a given value is allowed.
208+
// IsAllowed returns true if a given value is allowed.
209209
func (avs AllowedValues) IsAllowed(value string) bool {
210210
if len(avs) == 0 {
211211
// No configured allowed values, any value is allowed.
@@ -214,6 +214,37 @@ func (avs AllowedValues) IsAllowed(value string) bool {
214214
return common.StringSliceContains(avs.Values(), value)
215215
}
216216

217+
// IsExpectedEventType returns true if the event type is allowed for the given value.
218+
// This method can be used to check single values of event type or arrays of them.
219+
func (avs AllowedValues) IsExpectedEventType(value string, eventType interface{}) bool {
220+
expected := avs.ExpectedEventTypes(value)
221+
if len(expected) == 0 {
222+
// No restrictions defined, all good to go.
223+
return true
224+
}
225+
switch eventType := eventType.(type) {
226+
case string:
227+
return common.StringSliceContains(expected, eventType)
228+
case []interface{}:
229+
if len(eventType) == 0 {
230+
return false
231+
}
232+
for _, elem := range eventType {
233+
elem, ok := elem.(string)
234+
if !ok {
235+
return false
236+
}
237+
if !common.StringSliceContains(expected, elem) {
238+
return false
239+
}
240+
}
241+
return true
242+
default:
243+
// It must be a string, or an array of strings.
244+
return false
245+
}
246+
}
247+
217248
// Values returns the list of allowed values.
218249
func (avs AllowedValues) Values() []string {
219250
var values []string
@@ -223,6 +254,18 @@ func (avs AllowedValues) Values() []string {
223254
return values
224255
}
225256

257+
// ExpectedEventTypes returns the list of expected event types for a given value.
258+
func (avs AllowedValues) ExpectedEventTypes(value string) []string {
259+
for _, v := range avs {
260+
if v.Name == value {
261+
return v.ExpectedEventTypes
262+
}
263+
}
264+
265+
// If we are here, IsAllowed(value) is also false.
266+
return nil
267+
}
268+
226269
// AllowedValue is one of the allowed values for a field.
227270
type AllowedValue struct {
228271
Name string `yaml:"name"`

internal/fields/testdata/fields/fields.yml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,50 @@
1717
type: keyword
1818
normalize:
1919
- array
20+
- name: event.category
21+
type: keyword
22+
allowed_values:
23+
- name: authentication
24+
expected_event_types:
25+
- start
26+
- end
27+
- info
28+
- name: configuration
29+
expected_event_types:
30+
- access
31+
- change
32+
- creation
33+
- deletion
34+
- info
35+
- name: network
36+
expected_event_types:
37+
- access
38+
- allowed
39+
- connection
40+
- denied
41+
- end
42+
- info
43+
- protocol
44+
- start
45+
- name: event.type
46+
type: keyword
47+
normalize:
48+
- array
49+
allowed_values:
50+
- name: access
51+
- name: admin
52+
- name: allowed
53+
- name: change
54+
- name: connection
55+
- name: creation
56+
- name: deletion
57+
- name: denied
58+
- name: end
59+
- name: error
60+
- name: group
61+
- name: indicator
62+
- name: info
63+
- name: installation
64+
- name: protocol
65+
- name: start
66+
- name: user

internal/fields/validate.go

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import (
2626
"github.com/elastic/elastic-package/internal/packages/buildmanifest"
2727
)
2828

29+
var semver2_0_0 = semver.MustParse("2.0.0")
30+
2931
// Validator is responsible for fields validation.
3032
type Validator struct {
3133
// Schema contains definition records.
@@ -195,31 +197,27 @@ func (v *Validator) ValidateDocumentBody(body json.RawMessage) multierror.Error
195197
return errs
196198
}
197199

198-
errs := v.validateMapElement("", c)
199-
if len(errs) == 0 {
200-
return nil
201-
}
202-
return errs
200+
return v.ValidateDocumentMap(c)
203201
}
204202

205203
// ValidateDocumentMap validates the provided document as common.MapStr.
206204
func (v *Validator) ValidateDocumentMap(body common.MapStr) multierror.Error {
207-
errs := v.validateMapElement("", body)
205+
errs := v.validateMapElement("", body, body)
208206
if len(errs) == 0 {
209207
return nil
210208
}
211209
return errs
212210
}
213211

214-
func (v *Validator) validateMapElement(root string, elem common.MapStr) multierror.Error {
212+
func (v *Validator) validateMapElement(root string, elem common.MapStr, doc common.MapStr) multierror.Error {
215213
var errs multierror.Error
216214
for name, val := range elem {
217215
key := strings.TrimLeft(root+"."+name, ".")
218216

219217
switch val := val.(type) {
220218
case []map[string]interface{}:
221219
for _, m := range val {
222-
err := v.validateMapElement(key, m)
220+
err := v.validateMapElement(key, m, doc)
223221
if err != nil {
224222
errs = append(errs, err...)
225223
}
@@ -230,12 +228,12 @@ func (v *Validator) validateMapElement(root string, elem common.MapStr) multierr
230228
// because the entire object is mapped as a single field.
231229
continue
232230
}
233-
err := v.validateMapElement(key, val)
231+
err := v.validateMapElement(key, val, doc)
234232
if err != nil {
235233
errs = append(errs, err...)
236234
}
237235
default:
238-
err := v.validateScalarElement(key, val)
236+
err := v.validateScalarElement(key, val, doc)
239237
if err != nil {
240238
errs = append(errs, err)
241239
}
@@ -244,7 +242,7 @@ func (v *Validator) validateMapElement(root string, elem common.MapStr) multierr
244242
return errs
245243
}
246244

247-
func (v *Validator) validateScalarElement(key string, val interface{}) error {
245+
func (v *Validator) validateScalarElement(key string, val interface{}, doc common.MapStr) error {
248246
if key == "" {
249247
return nil // root key is always valid
250248
}
@@ -276,7 +274,7 @@ func (v *Validator) validateScalarElement(key string, val interface{}) error {
276274
return errors.Wrapf(err, "field %q is not normalized as expected", key)
277275
}
278276

279-
err = v.parseElementValue(key, *definition, val)
277+
err = v.parseElementValue(key, *definition, val, doc)
280278
if err != nil {
281279
return errors.Wrap(err, "parsing field value failed")
282280
}
@@ -384,7 +382,7 @@ func compareKeys(key string, def FieldDefinition, searchedKey string) bool {
384382

385383
func (v *Validator) validateExpectedNormalization(definition FieldDefinition, val interface{}) error {
386384
// Validate expected normalization starting with packages following spec v2 format.
387-
if v.specVersion.LessThan(semver.MustParse("2.0.0")) {
385+
if v.specVersion.LessThan(semver2_0_0) {
388386
return nil
389387
}
390388
for _, normalize := range definition.Normalize {
@@ -433,11 +431,11 @@ func validSubField(def FieldDefinition, extraPart string) bool {
433431

434432
// parseElementValue checks that the value stored in a field matches the field definition. For
435433
// arrays it checks it for each Element.
436-
func (v *Validator) parseElementValue(key string, definition FieldDefinition, val interface{}) error {
437-
return forEachElementValue(key, definition, val, v.parseSingleElementValue)
434+
func (v *Validator) parseElementValue(key string, definition FieldDefinition, val interface{}, doc common.MapStr) error {
435+
return forEachElementValue(key, definition, val, doc, v.parseSingleElementValue)
438436
}
439437

440-
func (v *Validator) parseSingleElementValue(key string, definition FieldDefinition, val interface{}) error {
438+
func (v *Validator) parseSingleElementValue(key string, definition FieldDefinition, val interface{}, doc common.MapStr) error {
441439
invalidTypeError := func() error {
442440
return fmt.Errorf("field %q's Go type, %T, does not match the expected field type: %s (field value: %v)", key, val, definition.Type, val)
443441
}
@@ -461,6 +459,11 @@ func (v *Validator) parseSingleElementValue(key string, definition FieldDefiniti
461459
if err := ensureAllowedValues(key, valStr, definition); err != nil {
462460
return err
463461
}
462+
if !v.specVersion.LessThan(semver2_0_0) {
463+
if err := ensureExpectedEventType(key, valStr, definition, doc); err != nil {
464+
return err
465+
}
466+
}
464467
// Normal text fields should be of type string.
465468
// If a pattern is provided, it checks if the value matches.
466469
case "keyword", "text":
@@ -475,6 +478,11 @@ func (v *Validator) parseSingleElementValue(key string, definition FieldDefiniti
475478
if err := ensureAllowedValues(key, valStr, definition); err != nil {
476479
return err
477480
}
481+
if !v.specVersion.LessThan(semver2_0_0) {
482+
if err := ensureExpectedEventType(key, valStr, definition, doc); err != nil {
483+
return err
484+
}
485+
}
478486
// Dates are expected to be formatted as strings or as seconds or milliseconds
479487
// since epoch.
480488
// If it is a string and a pattern is provided, it checks if the value matches.
@@ -563,13 +571,13 @@ func (v *Validator) isAllowedIPValue(s string) bool {
563571

564572
// forEachElementValue visits a function for each element in the given value if
565573
// it is an array. If it is not an array, it calls the function with it.
566-
func forEachElementValue(key string, definition FieldDefinition, val interface{}, fn func(string, FieldDefinition, interface{}) error) error {
574+
func forEachElementValue(key string, definition FieldDefinition, val interface{}, doc common.MapStr, fn func(string, FieldDefinition, interface{}, common.MapStr) error) error {
567575
arr, isArray := val.([]interface{})
568576
if !isArray {
569-
return fn(key, definition, val)
577+
return fn(key, definition, val, doc)
570578
}
571579
for _, element := range arr {
572-
err := fn(key, definition, element)
580+
err := fn(key, definition, element, doc)
573581
if err != nil {
574582
return err
575583
}
@@ -616,3 +624,14 @@ func ensureAllowedValues(key, value string, definition FieldDefinition) error {
616624
}
617625
return nil
618626
}
627+
628+
// ensureExpectedEventType validates that the document's `event.type` field is one of the expected
629+
// one for the given value.
630+
func ensureExpectedEventType(key, value string, definition FieldDefinition, doc common.MapStr) error {
631+
eventType, _ := doc.GetValue("event.type")
632+
if !definition.AllowedValues.IsExpectedEventType(value, eventType) {
633+
expected := definition.AllowedValues.ExpectedEventTypes(value)
634+
return fmt.Errorf("field \"event.type\" value \"%v\" (%T) is not one of the expected values (%s) for %s=%q", eventType, eventType, strings.Join(expected, ", "), key, value)
635+
}
636+
return nil
637+
}

internal/fields/validate_test.go

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111

1212
"github.com/stretchr/testify/assert"
1313
"github.com/stretchr/testify/require"
14+
15+
"github.com/elastic/elastic-package/internal/common"
1416
)
1517

1618
type results struct {
@@ -113,6 +115,56 @@ func TestValidate_WithSpecVersion(t *testing.T) {
113115
require.Empty(t, errs)
114116
}
115117

118+
func TestValidate_ExpectedEventType(t *testing.T) {
119+
validator, err := CreateValidatorForDirectory("testdata", WithSpecVersion("2.0.0"))
120+
require.NoError(t, err)
121+
require.NotNil(t, validator)
122+
123+
cases := []struct {
124+
title string
125+
doc common.MapStr
126+
valid bool
127+
}{
128+
{
129+
title: "valid event type",
130+
doc: common.MapStr{
131+
"event.category": "authentication",
132+
"event.type": []interface{}{"info"},
133+
},
134+
valid: true,
135+
},
136+
{
137+
title: "multiple valid event type",
138+
doc: common.MapStr{
139+
"event.category": "network",
140+
"event.type": []interface{}{"protocol", "connection", "end"},
141+
},
142+
valid: true,
143+
},
144+
{
145+
title: "unexpected event type",
146+
doc: common.MapStr{
147+
"event.category": "authentication",
148+
"event.type": []interface{}{"access"},
149+
},
150+
valid: false,
151+
},
152+
}
153+
154+
for _, c := range cases {
155+
t.Run(c.title, func(t *testing.T) {
156+
errs := validator.ValidateDocumentMap(c.doc)
157+
if c.valid {
158+
assert.Empty(t, errs)
159+
} else {
160+
if assert.Len(t, errs, 1) {
161+
assert.Contains(t, errs[0].Error(), "is not one of the expected values")
162+
}
163+
}
164+
})
165+
}
166+
}
167+
116168
func Test_parseElementValue(t *testing.T) {
117169
for _, test := range []struct {
118170
key string
@@ -433,7 +485,7 @@ func Test_parseElementValue(t *testing.T) {
433485
}
434486

435487
t.Run(test.key, func(t *testing.T) {
436-
err := v.parseElementValue(test.key, test.definition, test.value)
488+
err := v.parseElementValue(test.key, test.definition, test.value, common.MapStr{})
437489
if test.fail {
438490
require.Error(t, err)
439491
} else {

0 commit comments

Comments
 (0)