Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@ 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'],
});
});

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': [],
});
});
Expand All @@ -33,7 +34,7 @@ describe('buildObjectRecursive', () => {
},
};

expect(buildObjectRecursive('foo.barBaz', hit.fields)).toEqual({
expect(buildObjectRecursive('foo.barBaz', hit.fields, eventHitKeys)).toEqual({
foo: { barBaz: ['foo'] },
});
});
Expand All @@ -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'] }],
});
});
Expand All @@ -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: [
{
Expand All @@ -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: [
{
Expand Down Expand Up @@ -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: [
{
Expand Down Expand Up @@ -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
Expand All @@ -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: [
{
Expand All @@ -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: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Ecs> => {
const nestedParentPath = getNestedParentPath(fieldPath, fields);
export const buildObjectRecursive = (
fieldPath: string,
fields: Fields,
fieldsKeys: string[]
): Partial<Ecs> => {
const nestedParentPath = getNestedParentPath(fieldPath, fieldsKeys);
if (!nestedParentPath) {
return set({}, fieldPath, toStringArray(get(fieldPath, fields)));
}
Expand All @@ -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)))
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand All @@ -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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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}.`));