diff --git a/internal/fields/dependency_manager.go b/internal/fields/dependency_manager.go index 4221f064e5..d5a6e9d0e9 100644 --- a/internal/fields/dependency_manager.go +++ b/internal/fields/dependency_manager.go @@ -201,9 +201,8 @@ func (dm *DependencyManager) injectFieldsWithOptions(defs []common.MapStr, optio transformed.Delete("external") } - // Allow to override the type only from keyword to constant_keyword, - // to support the case of setting the value already in the mappings. - if ttype, _ := transformed["type"].(string); ttype != "constant_keyword" || imported.Type != "keyword" { + // Set the type back to the one imported, unless it is one of the allowed overrides. + if ttype, _ := transformed["type"].(string); !allowedTypeOverride(imported.Type, ttype) { transformed["type"] = imported.Type } @@ -264,6 +263,28 @@ func skipField(def common.MapStr) bool { return false } +func allowedTypeOverride(fromType, toType string) bool { + allowed := []struct { + from string + to string + }{ + // Support the case of setting the value already in the mappings. + {"keyword", "constant_keyword"}, + + // Support objects in ECS where the developer must decide if using + // a group or nested object. + {"object", "group"}, + {"object", "nested"}, + } + + for _, a := range allowed { + if a.from == fromType && a.to == toType { + return true + } + } + return false +} + // importField method resolves dependency on a single external field using available schemas. func (dm *DependencyManager) importField(schemaName, fieldPath string) (FieldDefinition, error) { if dm == nil { diff --git a/internal/fields/dependency_manager_test.go b/internal/fields/dependency_manager_test.go index 6ff488b2b4..15cc6d1ad2 100644 --- a/internal/fields/dependency_manager_test.go +++ b/internal/fields/dependency_manager_test.go @@ -725,6 +725,44 @@ func TestDependencyManagerWithECS(t *testing.T) { }, }, }, + { + title: "object to nested override", + defs: []common.MapStr{ + { + "name": "dns.answers", + "external": "ecs", + "type": "nested", + }, + }, + options: InjectFieldsOptions{}, + valid: true, + result: []common.MapStr{ + { + "name": "dns.answers", + "description": "An array containing an object for each answer section returned by the server.\nThe main keys that should be present in these objects are defined by ECS. Records that have more information may contain more keys than what ECS defines.\nNot all DNS data sources give all details about DNS answers. At minimum, answer objects must contain the `data` key. If more information is available, map as much of it to ECS as possible, and add any additional fields to the answer objects as custom fields.", + "type": "nested", + }, + }, + }, + { + title: "object to group override", + defs: []common.MapStr{ + { + "name": "dns.answers", + "external": "ecs", + "type": "group", + }, + }, + options: InjectFieldsOptions{}, + valid: true, + result: []common.MapStr{ + { + "name": "dns.answers", + "description": "An array containing an object for each answer section returned by the server.\nThe main keys that should be present in these objects are defined by ECS. Records that have more information may contain more keys than what ECS defines.\nNot all DNS data sources give all details about DNS answers. At minimum, answer objects must contain the `data` key. If more information is available, map as much of it to ECS as possible, and add any additional fields to the answer objects as custom fields.", + "type": "group", + }, + }, + }, } for _, c := range cases {