diff --git a/x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/build_ecs_objects.ts b/x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/build_ecs_objects.ts index 3b9891e79d9ab..ce97ee6d113c2 100644 --- a/x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/build_ecs_objects.ts +++ b/x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/build_ecs_objects.ts @@ -15,15 +15,16 @@ import { getNestedParentPath } from './get_nested_parent_path'; export const buildEcsObjects = (hit: EventHit): Ecs => { const ecsFields = [...TIMELINE_EVENTS_FIELDS]; + const fieldsKeys = Object.keys(hit.fields ?? {}); return ecsFields.reduce( (acc, field) => { - const nestedParentPath = getNestedParentPath(field, hit.fields); + const nestedParentPath = getNestedParentPath(field, fieldsKeys); if ( nestedParentPath != null || has(field, hit.fields) || ECS_METADATA_FIELDS.includes(field) ) { - return merge(acc, buildObjectRecursive(field, hit.fields)); + return merge(acc, buildObjectRecursive(field, hit.fields, fieldsKeys)); } return acc; }, diff --git a/x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/build_object_recursive.test.ts b/x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/build_object_recursive.test.ts index e6865780608a5..f2084e2c48c07 100644 --- a/x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/build_object_recursive.test.ts +++ b/x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/build_object_recursive.test.ts @@ -10,8 +10,9 @@ import { EventHit } from '../../../../../common/search_strategy'; import { buildObjectRecursive } from './build_object_recursive'; describe('buildObjectRecursive', () => { + const eventHitKeys = Object.keys(eventHit.fields ?? {}); it('builds an object from a single non-nested field', () => { - expect(buildObjectRecursive('@timestamp', eventHit.fields)).toEqual({ + expect(buildObjectRecursive('@timestamp', eventHit.fields, eventHitKeys)).toEqual({ '@timestamp': ['2020-11-17T14:48:08.922Z'], }); }); @@ -19,7 +20,7 @@ describe('buildObjectRecursive', () => { it('builds an object with no fields response', () => { const { fields, ...fieldLessHit } = eventHit; // @ts-expect-error fieldLessHit is intentionally missing fields - expect(buildObjectRecursive('@timestamp', fieldLessHit)).toEqual({ + expect(buildObjectRecursive('@timestamp', fieldLessHit, eventHitKeys)).toEqual({ '@timestamp': [], }); }); @@ -33,7 +34,7 @@ describe('buildObjectRecursive', () => { }, }; - expect(buildObjectRecursive('foo.barBaz', hit.fields)).toEqual({ + expect(buildObjectRecursive('foo.barBaz', hit.fields, eventHitKeys)).toEqual({ foo: { barBaz: ['foo'] }, }); }); @@ -45,7 +46,8 @@ describe('buildObjectRecursive', () => { foo: [{ bar: ['baz'] }], }, }; - expect(buildObjectRecursive('foo.bar', hit.fields)).toEqual({ + const hitKeys = Object.keys(hit.fields ?? {}); + expect(buildObjectRecursive('foo.bar', hit.fields, hitKeys)).toEqual({ foo: [{ bar: ['baz'] }], }); }); @@ -61,7 +63,8 @@ describe('buildObjectRecursive', () => { ], }, }; - expect(buildObjectRecursive('foo.bar.baz', nestedHit.fields)).toEqual({ + const nestedHitKeys = Object.keys(nestedHit.fields ?? {}); + expect(buildObjectRecursive('foo.bar.baz', nestedHit.fields, nestedHitKeys)).toEqual({ foo: { bar: [ { @@ -73,7 +76,9 @@ describe('buildObjectRecursive', () => { }); it('builds intermediate objects at multiple levels', () => { - expect(buildObjectRecursive('threat.enrichments.matched.atomic', eventHit.fields)).toEqual({ + expect( + buildObjectRecursive('threat.enrichments.matched.atomic', eventHit.fields, eventHitKeys) + ).toEqual({ threat: { enrichments: [ { @@ -117,7 +122,9 @@ describe('buildObjectRecursive', () => { }); it('preserves multiple values for a single leaf', () => { - expect(buildObjectRecursive('threat.enrichments.matched.field', eventHit.fields)).toEqual({ + expect( + buildObjectRecursive('threat.enrichments.matched.field', eventHit.fields, eventHitKeys) + ).toEqual({ threat: { enrichments: [ { @@ -162,6 +169,7 @@ describe('buildObjectRecursive', () => { describe('multiple levels of nested fields', () => { let nestedHit: EventHit; + let nestedHitKeys: string[]; beforeEach(() => { // @ts-expect-error nestedHit is minimal @@ -183,10 +191,13 @@ describe('buildObjectRecursive', () => { ], }, }; + nestedHitKeys = Object.keys(nestedHit.fields ?? {}); }); it('includes objects without the field', () => { - expect(buildObjectRecursive('nested_1.foo.nested_2.bar.leaf', nestedHit.fields)).toEqual({ + expect( + buildObjectRecursive('nested_1.foo.nested_2.bar.leaf', nestedHit.fields, nestedHitKeys) + ).toEqual({ nested_1: { foo: [ { @@ -205,7 +216,9 @@ describe('buildObjectRecursive', () => { }); it('groups multiple leaf values', () => { - expect(buildObjectRecursive('nested_1.foo.nested_2.bar.leaf_2', nestedHit.fields)).toEqual({ + expect( + buildObjectRecursive('nested_1.foo.nested_2.bar.leaf_2', nestedHit.fields, nestedHitKeys) + ).toEqual({ nested_1: { foo: [ { diff --git a/x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/build_object_recursive.ts b/x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/build_object_recursive.ts index 55284812d6d5a..96b9eef3a377d 100644 --- a/x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/build_object_recursive.ts +++ b/x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/build_object_recursive.ts @@ -12,8 +12,12 @@ import { Fields } from '../../../../../common/search_strategy'; import { toStringArray } from '../../../../../common/utils/to_array'; import { getNestedParentPath } from './get_nested_parent_path'; -export const buildObjectRecursive = (fieldPath: string, fields: Fields): Partial => { - const nestedParentPath = getNestedParentPath(fieldPath, fields); +export const buildObjectRecursive = ( + fieldPath: string, + fields: Fields, + fieldsKeys: string[] +): Partial => { + const nestedParentPath = getNestedParentPath(fieldPath, fieldsKeys); if (!nestedParentPath) { return set({}, fieldPath, toStringArray(get(fieldPath, fields))); } @@ -23,6 +27,6 @@ export const buildObjectRecursive = (fieldPath: string, fields: Fields): Partial return set( {}, nestedParentPath, - subFields.map((subField) => buildObjectRecursive(subPath, subField)) + subFields.map((subField) => buildObjectRecursive(subPath, subField, Object.keys(subField))) ); }; diff --git a/x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/format_timeline_data.ts b/x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/format_timeline_data.ts index 3b83bb0873981..711c7f9c8d874 100644 --- a/x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/format_timeline_data.ts +++ b/x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/format_timeline_data.ts @@ -109,9 +109,10 @@ export const formatTimelineData = async ( } result.node.data = []; + const hitFieldKeys = Object.keys(hit.fields || {}); for (const fieldName of uniqueFields) { - const nestedParentPath = getNestedParentPath(fieldName, hit.fields); + const nestedParentPath = getNestedParentPath(fieldName, hitFieldKeys); const isEcs = ECS_METADATA_FIELDS.includes(fieldName); if (!nestedParentPath && !has(fieldName, hit.fields) && !isEcs) { // eslint-disable-next-line no-continue @@ -127,7 +128,7 @@ export const formatTimelineData = async ( } if (ecsFieldSet.has(fieldName)) { - deepMerge(result.node.ecs, buildObjectRecursive(fieldName, hit.fields)); + deepMerge(result.node.ecs, buildObjectRecursive(fieldName, hit.fields, hitFieldKeys)); } } diff --git a/x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/get_nested_parent_path.test.ts b/x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/get_nested_parent_path.test.ts index ad923ed8ce954..52667ec4e3628 100644 --- a/x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/get_nested_parent_path.test.ts +++ b/x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/get_nested_parent_path.test.ts @@ -9,6 +9,7 @@ import { getNestedParentPath } from './get_nested_parent_path'; describe('getNestedParentPath', () => { let testFields: Fields | undefined; + let testFieldsKeys: string[]; beforeAll(() => { testFields = { 'not.nested': ['I am not nested'], @@ -18,22 +19,23 @@ describe('getNestedParentPath', () => { }, ], }; + testFieldsKeys = Object.keys(testFields); }); it('should ignore fields that are not nested', () => { const notNestedPath = 'not.nested'; - const shouldBeUndefined = getNestedParentPath(notNestedPath, testFields); + const shouldBeUndefined = getNestedParentPath(notNestedPath, testFieldsKeys); expect(shouldBeUndefined).toBe(undefined); }); it('should capture fields that are nested', () => { const nestedPath = 'is.nested.field'; - const nestedParentPath = getNestedParentPath(nestedPath, testFields); + const nestedParentPath = getNestedParentPath(nestedPath, testFieldsKeys); expect(nestedParentPath).toEqual('is.nested'); }); it('should return undefined when the `fields` param is undefined', () => { const nestedPath = 'is.nested.field'; - expect(getNestedParentPath(nestedPath, undefined)).toBe(undefined); + expect(getNestedParentPath(nestedPath, [])).toBe(undefined); }); }); diff --git a/x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/get_nested_parent_path.ts b/x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/get_nested_parent_path.ts index f2e769229fc1b..6b865628821b2 100644 --- a/x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/get_nested_parent_path.ts +++ b/x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/get_nested_parent_path.ts @@ -5,14 +5,8 @@ * 2.0. */ -import { Fields } from '../../../../../common/search_strategy'; - /** * If a prefix of our full field path is present as a field, we know that our field is nested */ -export const getNestedParentPath = ( - fieldPath: string, - fields: Fields | undefined -): string | undefined => - fields && - Object.keys(fields).find((field) => field !== fieldPath && fieldPath.startsWith(`${field}.`)); +export const getNestedParentPath = (fieldPath: string, fields: string[]): string | undefined => + fields.find((field) => field !== fieldPath && fieldPath.startsWith(`${field}.`));