diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts b/x-pack/plugins/security_solution/common/utils/field_formatters.test.ts similarity index 97% rename from x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts rename to x-pack/plugins/security_solution/common/utils/field_formatters.test.ts index dc3efc6909c63..b724c0f672b50 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts +++ b/x-pack/plugins/security_solution/common/utils/field_formatters.test.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { EventHit, EventSource } from '../../../../../../common/search_strategy'; -import { getDataFromFieldsHits, getDataFromSourceHits, getDataSafety } from './helpers'; -import { eventDetailsFormattedFields, eventHit } from '../mocks'; +import { EventHit, EventSource } from '../search_strategy'; +import { getDataFromFieldsHits, getDataFromSourceHits, getDataSafety } from './field_formatters'; +import { eventDetailsFormattedFields, eventHit } from './mock_event_details'; describe('Events Details Helpers', () => { const fields: EventHit['fields'] = eventHit.fields; diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts b/x-pack/plugins/security_solution/common/utils/field_formatters.ts similarity index 96% rename from x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts rename to x-pack/plugins/security_solution/common/utils/field_formatters.ts index 2fc729729e435..b436f8e616122 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts +++ b/x-pack/plugins/security_solution/common/utils/field_formatters.ts @@ -7,12 +7,8 @@ import { get, isEmpty, isNumber, isObject, isString } from 'lodash/fp'; -import { - EventHit, - EventSource, - TimelineEventsDetailsItem, -} from '../../../../../../common/search_strategy'; -import { toObjectArrayOfStrings, toStringArray } from '../../../../helpers/to_array'; +import { EventHit, EventSource, TimelineEventsDetailsItem } from '../search_strategy'; +import { toObjectArrayOfStrings, toStringArray } from './to_array'; export const baseCategoryFields = ['@timestamp', 'labels', 'message', 'tags']; diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/mocks.ts b/x-pack/plugins/security_solution/common/utils/mock_event_details.ts similarity index 100% rename from x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/mocks.ts rename to x-pack/plugins/security_solution/common/utils/mock_event_details.ts diff --git a/x-pack/plugins/security_solution/server/search_strategy/helpers/to_array.ts b/x-pack/plugins/security_solution/common/utils/to_array.ts similarity index 100% rename from x-pack/plugins/security_solution/server/search_strategy/helpers/to_array.ts rename to x-pack/plugins/security_solution/common/utils/to_array.ts diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/components/event_details/__mocks__/index.ts index ba0567c40eb92..3edd6e6fda14b 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/__mocks__/index.ts @@ -655,4 +655,16 @@ export const mockAlertDetailsData = [ values: ['7.10.0'], originalValue: ['7.10.0'], }, + { + category: 'threat', + field: 'threat.indicator', + values: [`{"first_seen":"2021-03-25T18:17:00.000Z"}`], + originalValue: [`{"first_seen":"2021-03-25T18:17:00.000Z"}`], + }, + { + category: 'threat', + field: 'threat.indicator.matched', + values: `["file", "url"]`, + originalValue: ['file', 'url'], + }, ]; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx similarity index 90% rename from x-pack/plugins/security_solution/public/common/components/event_details/summary_view.test.tsx rename to x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx index c19a3952220cf..b8f29996d603b 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { waitFor } from '@testing-library/react'; -import { SummaryViewComponent } from './summary_view'; +import { AlertSummaryView } from './alert_summary_view'; import { mockAlertDetailsData } from './__mocks__'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; import { useRuleAsync } from '../../../detections/containers/detection_engine/rules/use_rule_async'; @@ -30,7 +30,7 @@ const props = { timelineId: 'detections-page', }; -describe('SummaryViewComponent', () => { +describe('AlertSummaryView', () => { const mount = useMountAppended(); beforeEach(() => { @@ -44,7 +44,7 @@ describe('SummaryViewComponent', () => { test('render correct items', () => { const wrapper = mount( - + ); expect(wrapper.find('[data-test-subj="summary-view"]').exists()).toEqual(true); @@ -53,7 +53,7 @@ describe('SummaryViewComponent', () => { test('render investigation guide', async () => { const wrapper = mount( - + ); await waitFor(() => { @@ -69,7 +69,7 @@ describe('SummaryViewComponent', () => { }); const wrapper = mount( - + ); await waitFor(() => { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx new file mode 100644 index 0000000000000..091049b967f02 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx @@ -0,0 +1,200 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiBasicTableColumn, + EuiDescriptionList, + EuiDescriptionListDescription, + EuiDescriptionListTitle, +} from '@elastic/eui'; +import { get, getOr } from 'lodash/fp'; +import React, { useMemo } from 'react'; +import styled from 'styled-components'; +import { FormattedFieldValue } from '../../../timelines/components/timeline/body/renderers/formatted_field'; +import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; +import { BrowserFields } from '../../../../common/search_strategy/index_fields'; +import { + ALERTS_HEADERS_RISK_SCORE, + ALERTS_HEADERS_RULE, + ALERTS_HEADERS_SEVERITY, + ALERTS_HEADERS_THRESHOLD_CARDINALITY, + ALERTS_HEADERS_THRESHOLD_COUNT, + ALERTS_HEADERS_THRESHOLD_TERMS, +} from '../../../detections/components/alerts_table/translations'; +import { + IP_FIELD_TYPE, + SIGNAL_RULE_NAME_FIELD_NAME, +} from '../../../timelines/components/timeline/body/renderers/constants'; +import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../../../network/components/ip'; +import { SummaryView } from './summary_view'; +import { AlertSummaryRow, getSummaryColumns, SummaryRow } from './helpers'; +import { useRuleAsync } from '../../../detections/containers/detection_engine/rules/use_rule_async'; +import * as i18n from './translations'; +import { LineClamp } from '../line_clamp'; + +const StyledEuiDescriptionList = styled(EuiDescriptionList)` + padding: 24px 4px 4px; +`; + +const fields = [ + { id: 'signal.status' }, + { id: '@timestamp' }, + { + id: SIGNAL_RULE_NAME_FIELD_NAME, + linkField: 'signal.rule.id', + label: ALERTS_HEADERS_RULE, + }, + { id: 'signal.rule.severity', label: ALERTS_HEADERS_SEVERITY }, + { id: 'signal.rule.risk_score', label: ALERTS_HEADERS_RISK_SCORE }, + { id: 'host.name' }, + { id: 'user.name' }, + { id: SOURCE_IP_FIELD_NAME, fieldType: IP_FIELD_TYPE }, + { id: DESTINATION_IP_FIELD_NAME, fieldType: IP_FIELD_TYPE }, + { id: 'signal.threshold_result.count', label: ALERTS_HEADERS_THRESHOLD_COUNT }, + { id: 'signal.threshold_result.terms', label: ALERTS_HEADERS_THRESHOLD_TERMS }, + { id: 'signal.threshold_result.cardinality', label: ALERTS_HEADERS_THRESHOLD_CARDINALITY }, +]; + +const getDescription = ({ + contextId, + eventId, + fieldName, + value, + fieldType = '', + linkValue, +}: AlertSummaryRow['description']) => ( + +); + +const getSummaryRows = ({ + data, + browserFields, + timelineId, + eventId, +}: { + data: TimelineEventsDetailsItem[]; + browserFields: BrowserFields; + timelineId: string; + eventId: string; +}) => { + return data != null + ? fields.reduce((acc, item) => { + const field = data.find((d) => d.field === item.id); + if (!field) { + return acc; + } + const linkValueField = + item.linkField != null && data.find((d) => d.field === item.linkField); + const linkValue = getOr(null, 'originalValue.0', linkValueField); + const value = getOr(null, 'originalValue.0', field); + const category = field.category; + const fieldType = get(`${category}.fields.${field.field}.type`, browserFields) as string; + const description = { + contextId: timelineId, + eventId, + fieldName: item.id, + value, + fieldType: item.fieldType ?? fieldType, + linkValue: linkValue ?? undefined, + }; + + if (item.id === 'signal.threshold_result.terms') { + try { + const terms = getOr(null, 'originalValue', field); + const parsedValue = terms.map((term: string) => JSON.parse(term)); + const thresholdTerms = (parsedValue ?? []).map( + (entry: { field: string; value: string }) => { + return { + title: `${entry.field} [threshold]`, + description: { + ...description, + value: entry.value, + }, + }; + } + ); + return [...acc, ...thresholdTerms]; + } catch (err) { + return acc; + } + } + + if (item.id === 'signal.threshold_result.cardinality') { + try { + const parsedValue = JSON.parse(value); + return [ + ...acc, + { + title: ALERTS_HEADERS_THRESHOLD_CARDINALITY, + description: { + ...description, + value: `count(${parsedValue.field}) == ${parsedValue.value}`, + }, + }, + ]; + } catch (err) { + return acc; + } + } + + return [ + ...acc, + { + title: item.label ?? item.id, + description, + }, + ]; + }, []) + : []; +}; + +const summaryColumns: Array> = getSummaryColumns(getDescription); + +const AlertSummaryViewComponent: React.FC<{ + browserFields: BrowserFields; + data: TimelineEventsDetailsItem[]; + eventId: string; + timelineId: string; +}> = ({ browserFields, data, eventId, timelineId }) => { + const summaryRows = useMemo(() => getSummaryRows({ browserFields, data, eventId, timelineId }), [ + browserFields, + data, + eventId, + timelineId, + ]); + + const ruleId = useMemo(() => { + const item = data.find((d) => d.field === 'signal.rule.id'); + return Array.isArray(item?.originalValue) + ? item?.originalValue[0] + : item?.originalValue ?? null; + }, [data]); + const { rule: maybeRule } = useRuleAsync(ruleId); + + return ( + <> + + {maybeRule?.note && ( + + {i18n.INVESTIGATION_GUIDE} + + + + + )} + + ); +}; + +export const AlertSummaryView = React.memo(AlertSummaryViewComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx index 3fe8f8a147ea8..197022ce316c5 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx @@ -12,7 +12,7 @@ import '../../mock/match_media'; import '../../mock/react_beautiful_dnd'; import { mockDetailItemData, mockDetailItemDataId, TestProviders } from '../../mock'; -import { EventDetails, EventsViewType } from './event_details'; +import { EventDetails, EventsViewType, EventView, ThreatView } from './event_details'; import { mockBrowserFields } from '../../containers/source/mock'; import { useMountAppended } from '../../utils/use_mount_appended'; import { mockAlertDetailsData } from './__mocks__'; @@ -28,10 +28,12 @@ describe('EventDetails', () => { data: mockDetailItemData, id: mockDetailItemDataId, isAlert: false, - onViewSelected: jest.fn(), + onEventViewSelected: jest.fn(), + onThreatViewSelected: jest.fn(), timelineTabType: TimelineTabs.query, timelineId: 'test', - view: EventsViewType.summaryView, + eventView: EventsViewType.summaryView as EventView, + threatView: EventsViewType.threatSummaryView as ThreatView, }; const alertsProps = { @@ -97,4 +99,27 @@ describe('EventDetails', () => { ).toEqual('Summary'); }); }); + + describe('threat tabs', () => { + ['Threat Summary', 'Threat Details'].forEach((tab) => { + test(`it renders the ${tab} tab`, () => { + expect( + alertsWrapper + .find('[data-test-subj="threatDetails"]') + .find('[role="tablist"]') + .containsMatchingElement({tab}) + ).toBeTruthy(); + }); + }); + + test('the Summary tab is selected by default', () => { + expect( + alertsWrapper + .find('[data-test-subj="threatDetails"]') + .find('.euiTab-isSelected') + .first() + .text() + ).toEqual('Threat Summary'); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index 4979d70ce2d7b..0e4cf7f4ae2fe 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -14,14 +14,23 @@ import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/ti import { EventFieldsBrowser } from './event_fields_browser'; import { JsonView } from './json_view'; import * as i18n from './translations'; -import { SummaryView } from './summary_view'; +import { AlertSummaryView } from './alert_summary_view'; +import { ThreatSummaryView } from './threat_summary_view'; +import { ThreatDetailsView } from './threat_details_view'; import { TimelineTabs } from '../../../../common/types/timeline'; +import { INDICATOR_DESTINATION_PATH } from '../../../../common/constants'; -export type View = EventsViewType.tableView | EventsViewType.jsonView | EventsViewType.summaryView; +export type EventView = + | EventsViewType.tableView + | EventsViewType.jsonView + | EventsViewType.summaryView; +export type ThreatView = EventsViewType.threatSummaryView | EventsViewType.threatDetailsView; export enum EventsViewType { tableView = 'table-view', jsonView = 'json-view', summaryView = 'summary-view', + threatSummaryView = 'threat-summary-view', + threatDetailsView = 'threat-details-view', } interface Props { @@ -29,8 +38,10 @@ interface Props { data: TimelineEventsDetailsItem[]; id: string; isAlert: boolean; - view: EventsViewType; - onViewSelected: (selected: EventsViewType) => void; + eventView: EventView; + threatView: ThreatView; + onEventViewSelected: (selected: EventView) => void; + onThreatViewSelected: (selected: ThreatView) => void; timelineTabType: TimelineTabs | 'flyout'; timelineId: string; } @@ -45,7 +56,16 @@ const StyledEuiTabbedContent = styled(EuiTabbedContent)` display: flex; flex: 1; flex-direction: column; - overflow: hidden; + overflow: scroll; + ::-webkit-scrollbar { + -webkit-appearance: none; + width: 7px; + } + ::-webkit-scrollbar-thumb { + border-radius: 4px; + background-color: rgba(0, 0, 0, 0.5); + -webkit-box-shadow: 0 0 1px rgba(255, 255, 255, 0.5); + } } `; @@ -57,14 +77,19 @@ const TabContentWrapper = styled.div` const EventDetailsComponent: React.FC = ({ browserFields, data, + eventView, id, - view, - onViewSelected, - timelineTabType, - timelineId, isAlert, + onEventViewSelected, + onThreatViewSelected, + threatView, + timelineId, + timelineTabType, }) => { - const handleTabClick = useCallback((e) => onViewSelected(e.id), [onViewSelected]); + const handleEventTabClick = useCallback((e) => onEventViewSelected(e.id), [onEventViewSelected]); + const handleThreatTabClick = useCallback((e) => onThreatViewSelected(e.id), [ + onThreatViewSelected, + ]); const alerts = useMemo( () => [ @@ -74,11 +99,13 @@ const EventDetailsComponent: React.FC = ({ content: ( <> - ), @@ -122,15 +149,60 @@ const EventDetailsComponent: React.FC = ({ [alerts, browserFields, data, id, isAlert, timelineId, timelineTabType] ); - const selectedTab = useMemo(() => tabs.find((t) => t.id === view) ?? tabs[0], [tabs, view]); + const selectedEventTab = useMemo(() => tabs.find((t) => t.id === eventView) ?? tabs[0], [ + tabs, + eventView, + ]); + + const isThreatPresent: boolean = useMemo( + () => + selectedEventTab.id === tabs[0].id && + isAlert && + data.some((item) => item.field === INDICATOR_DESTINATION_PATH), + [tabs, selectedEventTab, isAlert, data] + ); + + const threatTabs: EuiTabbedContentTab[] = useMemo(() => { + return isAlert && isThreatPresent + ? [ + { + id: EventsViewType.threatSummaryView, + name: i18n.THREAT_SUMMARY, + content: , + }, + { + id: EventsViewType.threatDetailsView, + name: i18n.THREAT_DETAILS, + content: , + }, + ] + : []; + }, [data, id, isAlert, timelineId, isThreatPresent]); + + const selectedThreatTab = useMemo( + () => threatTabs.find((t) => t.id === threatView) ?? threatTabs[0], + [threatTabs, threatView] + ); return ( - + <> + + {isThreatPresent && ( + + )} + ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx index 00e2ee276f181..67e67584849cc 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx @@ -7,6 +7,8 @@ import { get, getOr, isEmpty, uniqBy } from 'lodash/fp'; +import React from 'react'; +import { EuiBasicTableColumn, EuiTitle } from '@elastic/eui'; import { elementOrChildrenHasFocus, getFocusedDataColindexCell, @@ -51,6 +53,38 @@ export interface Item { values: ToStringArray; } +export interface AlertSummaryRow { + title: string; + description: { + contextId: string; + eventId: string; + fieldName: string; + value: string; + fieldType: string; + linkValue: string | undefined; + }; +} + +export interface ThreatSummaryRow { + title: string; + description: { + contextId: string; + eventId: string; + fieldName: string; + values: string[]; + }; +} + +export interface ThreatDetailsRow { + title: string; + description: { + fieldName: string; + value: string; + }; +} + +export type SummaryRow = AlertSummaryRow | ThreatSummaryRow | ThreatDetailsRow; + export const getColumnHeaderFromBrowserField = ({ browserField, width = DEFAULT_COLUMN_MIN_WIDTH, @@ -172,3 +206,33 @@ export const onEventDetailsTabKeyPressed = ({ }); } }; + +const getTitle = (title: string) => ( + +
{title}
+
+); +getTitle.displayName = 'getTitle'; + +export const getSummaryColumns = ( + DescriptionComponent: + | React.FC + | React.FC + | React.FC +): Array> => { + return [ + { + field: 'title', + truncateText: false, + render: getTitle, + width: '120px', + name: '', + }, + { + field: 'description', + truncateText: false, + render: DescriptionComponent, + name: '', + }, + ]; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx index 8e07910c1c071..3b2c55e9a6b67 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx @@ -5,69 +5,11 @@ * 2.0. */ -import { get, getOr } from 'lodash/fp'; -import { - EuiTitle, - EuiDescriptionList, - EuiDescriptionListTitle, - EuiDescriptionListDescription, - EuiInMemoryTable, - EuiBasicTableColumn, -} from '@elastic/eui'; -import React, { useMemo } from 'react'; +import { EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui'; +import React from 'react'; import styled from 'styled-components'; -import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; -import { FormattedFieldValue } from '../../../timelines/components/timeline/body/renderers/formatted_field'; -import * as i18n from './translations'; -import { BrowserFields } from '../../../../common/search_strategy/index_fields'; -import { - ALERTS_HEADERS_RISK_SCORE, - ALERTS_HEADERS_RULE, - ALERTS_HEADERS_SEVERITY, - ALERTS_HEADERS_THRESHOLD_COUNT, - ALERTS_HEADERS_THRESHOLD_TERMS, - ALERTS_HEADERS_THRESHOLD_CARDINALITY, -} from '../../../detections/components/alerts_table/translations'; -import { - IP_FIELD_TYPE, - SIGNAL_RULE_NAME_FIELD_NAME, -} from '../../../timelines/components/timeline/body/renderers/constants'; -import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../../../network/components/ip'; -import { LineClamp } from '../line_clamp'; -import { useRuleAsync } from '../../../detections/containers/detection_engine/rules/use_rule_async'; - -interface SummaryRow { - title: string; - description: { - contextId: string; - eventId: string; - fieldName: string; - value: string; - fieldType: string; - linkValue: string | undefined; - }; -} -type Summary = SummaryRow[]; - -const fields = [ - { id: 'signal.status' }, - { id: '@timestamp' }, - { - id: SIGNAL_RULE_NAME_FIELD_NAME, - linkField: 'signal.rule.id', - label: ALERTS_HEADERS_RULE, - }, - { id: 'signal.rule.severity', label: ALERTS_HEADERS_SEVERITY }, - { id: 'signal.rule.risk_score', label: ALERTS_HEADERS_RISK_SCORE }, - { id: 'host.name' }, - { id: 'user.name' }, - { id: SOURCE_IP_FIELD_NAME, fieldType: IP_FIELD_TYPE }, - { id: DESTINATION_IP_FIELD_NAME, fieldType: IP_FIELD_TYPE }, - { id: 'signal.threshold_result.count', label: ALERTS_HEADERS_THRESHOLD_COUNT }, - { id: 'signal.threshold_result.terms', label: ALERTS_HEADERS_THRESHOLD_TERMS }, - { id: 'signal.threshold_result.cardinality', label: ALERTS_HEADERS_THRESHOLD_CARDINALITY }, -]; +import { SummaryRow } from './helpers'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)` @@ -77,173 +19,26 @@ const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)` .euiTableRowCell { border: none; } -`; -const StyledEuiDescriptionList = styled(EuiDescriptionList)` - padding: 24px 4px 4px; + .euiTableCellContent { + display: flex; + flex-direction: column; + align-items: flex-start; + } `; -const getTitle = (title: SummaryRow['title']) => ( - -
{title}
-
-); - -getTitle.displayName = 'getTitle'; - -const getDescription = ({ - contextId, - eventId, - fieldName, - value, - fieldType = '', - linkValue, -}: SummaryRow['description']) => ( - -); - -const getSummary = ({ - data, - browserFields, - timelineId, - eventId, -}: { - data: TimelineEventsDetailsItem[]; - browserFields: BrowserFields; - timelineId: string; - eventId: string; -}) => { - return data != null - ? fields.reduce((acc, item) => { - const field = data.find((d) => d.field === item.id); - if (!field) { - return acc; - } - const linkValueField = - item.linkField != null && data.find((d) => d.field === item.linkField); - const linkValue = getOr(null, 'originalValue.0', linkValueField); - const value = getOr(null, 'originalValue.0', field); - const category = field.category; - const fieldType = get(`${category}.fields.${field.field}.type`, browserFields) as string; - const description = { - contextId: timelineId, - eventId, - fieldName: item.id, - value, - fieldType: item.fieldType ?? fieldType, - linkValue: linkValue ?? undefined, - }; - - if (item.id === 'signal.threshold_result.terms') { - try { - const terms = getOr(null, 'originalValue', field); - const parsedValue = terms.map((term: string) => JSON.parse(term)); - const thresholdTerms = (parsedValue ?? []).map( - (entry: { field: string; value: string }) => { - return { - title: `${entry.field} [threshold]`, - description: { - ...description, - value: entry.value, - }, - }; - } - ); - return [...acc, ...thresholdTerms]; - } catch (err) { - return acc; - } - } - - if (item.id === 'signal.threshold_result.cardinality') { - try { - const parsedValue = JSON.parse(value); - return [ - ...acc, - { - title: ALERTS_HEADERS_THRESHOLD_CARDINALITY, - description: { - ...description, - value: `count(${parsedValue.field}) == ${parsedValue.value}`, - }, - }, - ]; - } catch (err) { - return acc; - } - } - - return [ - ...acc, - { - title: item.label ?? item.id, - description, - }, - ]; - }, []) - : []; -}; - -const summaryColumns: Array> = [ - { - field: 'title', - truncateText: false, - render: getTitle, - width: '120px', - name: '', - }, - { - field: 'description', - truncateText: false, - render: getDescription, - name: '', - }, -]; - export const SummaryViewComponent: React.FC<{ - browserFields: BrowserFields; - data: TimelineEventsDetailsItem[]; - eventId: string; - timelineId: string; -}> = ({ data, eventId, timelineId, browserFields }) => { - const ruleId = useMemo(() => { - const item = data.find((d) => d.field === 'signal.rule.id'); - return Array.isArray(item?.originalValue) - ? item?.originalValue[0] - : item?.originalValue ?? null; - }, [data]); - const { rule: maybeRule } = useRuleAsync(ruleId); - const summaryList = useMemo(() => getSummary({ browserFields, data, eventId, timelineId }), [ - browserFields, - data, - eventId, - timelineId, - ]); - + summaryColumns: Array>; + summaryRows: SummaryRow[]; + dataTestSubj?: string; +}> = ({ summaryColumns, summaryRows, dataTestSubj = 'summary-view' }) => { return ( - <> - - {maybeRule?.note && ( - - {i18n.INVESTIGATION_GUIDE} - - - - - )} - + ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.test.tsx new file mode 100644 index 0000000000000..81bffe9b66638 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { ThreatDetailsView } from './threat_details_view'; +import { mockAlertDetailsData } from './__mocks__'; +import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; + +import { TestProviders } from '../../mock'; +import { useMountAppended } from '../../utils/use_mount_appended'; + +jest.mock('../../../detections/containers/detection_engine/rules/use_rule_async', () => { + return { + useRuleAsync: jest.fn(), + }; +}); + +const props = { + data: mockAlertDetailsData as TimelineEventsDetailsItem[], + eventId: '5d1d53da502f56aacc14c3cb5c669363d102b31f99822e5d369d4804ed370a31', + timelineId: 'detections-page', +}; + +describe('ThreatDetailsView', () => { + const mount = useMountAppended(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('render correct items', () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="threat-details-view-0"]').exists()).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.tsx new file mode 100644 index 0000000000000..0889986237442 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiBasicTableColumn, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiToolTip, +} from '@elastic/eui'; +import React, { useMemo } from 'react'; + +import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; +import { SummaryView } from './summary_view'; +import { getSummaryColumns, SummaryRow, ThreatDetailsRow } from './helpers'; +import { getDataFromSourceHits } from '../../../../common/utils/field_formatters'; +import { INDICATOR_DESTINATION_PATH } from '../../../../common/constants'; + +const ThreatDetailsDescription: React.FC = ({ + fieldName, + value, +}) => ( + + + {fieldName} + + + } + > + {value} + +); + +const getSummaryRowsArray = ({ + data, +}: { + data: TimelineEventsDetailsItem[]; +}): ThreatDetailsRow[][] => { + if (!data) return [[]]; + const threatInfo = data.find( + ({ field, originalValue }) => field === INDICATOR_DESTINATION_PATH && originalValue + ); + if (!threatInfo) return [[]]; + const { originalValue } = threatInfo; + const values = Array.isArray(originalValue) ? originalValue : [originalValue]; + return values.map((value) => + getDataFromSourceHits(JSON.parse(value)).map((threatInfoItem) => ({ + title: threatInfoItem.field.replace(`${INDICATOR_DESTINATION_PATH}.`, ''), + description: { fieldName: threatInfoItem.field, value: threatInfoItem.originalValue }, + })) + ); +}; + +const summaryColumns: Array> = getSummaryColumns( + ThreatDetailsDescription +); + +const ThreatDetailsViewComponent: React.FC<{ + data: TimelineEventsDetailsItem[]; +}> = ({ data }) => { + const summaryRowsArray = useMemo(() => getSummaryRowsArray({ data }), [data]); + return ( + <> + {summaryRowsArray.map((summaryRows, index, arr) => { + const key = summaryRows.find((threat) => threat.title === 'matched.id')?.description + .value[0]; + return ( +
+ + {index < arr.length - 1 && } +
+ ); + })} + + ); +}; + +export const ThreatDetailsView = React.memo(ThreatDetailsViewComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.test.tsx new file mode 100644 index 0000000000000..756fc7d32b371 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { ThreatSummaryView } from './threat_summary_view'; +import { mockAlertDetailsData } from './__mocks__'; +import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; + +import { TestProviders } from '../../mock'; +import { useMountAppended } from '../../utils/use_mount_appended'; + +jest.mock('../../../detections/containers/detection_engine/rules/use_rule_async', () => { + return { + useRuleAsync: jest.fn(), + }; +}); + +const props = { + data: mockAlertDetailsData as TimelineEventsDetailsItem[], + eventId: '5d1d53da502f56aacc14c3cb5c669363d102b31f99822e5d369d4804ed370a31', + timelineId: 'detections-page', +}; + +describe('ThreatSummaryView', () => { + const mount = useMountAppended(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('render correct items', () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="threat-summary-view"]').exists()).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.tsx new file mode 100644 index 0000000000000..96ae2071c449b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiBasicTableColumn } from '@elastic/eui'; +import React, { useMemo } from 'react'; + +import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; +import { FormattedFieldValue } from '../../../timelines/components/timeline/body/renderers/formatted_field'; +import { BrowserFields } from '../../../../common/search_strategy/index_fields'; +import { SummaryView } from './summary_view'; +import { getSummaryColumns, SummaryRow, ThreatSummaryRow } from './helpers'; +import { INDICATOR_DESTINATION_PATH } from '../../../../common/constants'; + +const getDescription = ({ + contextId, + eventId, + fieldName, + values, +}: ThreatSummaryRow['description']): JSX.Element => ( + <> + {values.map((value: string) => ( + + ))} + +); + +const getSummaryRows = ({ + data, + timelineId: contextId, + eventId, +}: { + data: TimelineEventsDetailsItem[]; + browserFields?: BrowserFields; + timelineId: string; + eventId: string; +}) => { + if (!data) return []; + return data.reduce((acc, { field, originalValue }) => { + if (field.startsWith(`${INDICATOR_DESTINATION_PATH}.`) && originalValue) { + return [ + ...acc, + { + title: field.replace(`${INDICATOR_DESTINATION_PATH}.`, ''), + description: { + values: Array.isArray(originalValue) ? originalValue : [originalValue], + contextId, + eventId, + fieldName: field, + }, + }, + ]; + } + return acc; + }, []); +}; + +const summaryColumns: Array> = getSummaryColumns(getDescription); + +const ThreatSummaryViewComponent: React.FC<{ + data: TimelineEventsDetailsItem[]; + eventId: string; + timelineId: string; +}> = ({ data, eventId, timelineId }) => { + const summaryRows = useMemo(() => getSummaryRows({ data, eventId, timelineId }), [ + data, + eventId, + timelineId, + ]); + + return ( + + ); +}; + +export const ThreatSummaryView = React.memo(ThreatSummaryViewComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts index 3a599b174251a..73a2e0d57307c 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts @@ -11,6 +11,14 @@ export const SUMMARY = i18n.translate('xpack.securitySolution.alertDetails.summa defaultMessage: 'Summary', }); +export const THREAT_SUMMARY = i18n.translate('xpack.securitySolution.alertDetails.threatSummary', { + defaultMessage: 'Threat Summary', +}); + +export const THREAT_DETAILS = i18n.translate('xpack.securitySolution.alertDetails.threatDetails', { + defaultMessage: 'Threat Details', +}); + export const INVESTIGATION_GUIDE = i18n.translate( 'xpack.securitySolution.alertDetails.summary.investigationGuide', { diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap index 87392bce3ee63..50970304953ca 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap @@ -262,7 +262,7 @@ Array [ -ms-flex: 1; flex: 1; overflow: hidden; - padding: 4px 16px 64px; + padding: 4px 16px 50px; } .c0 { @@ -537,7 +537,7 @@ Array [ -ms-flex: 1; flex: 1; overflow: hidden; - padding: 4px 16px 64px; + padding: 4px 16px 50px; } .c0 { @@ -806,7 +806,7 @@ Array [ -ms-flex: 1; flex: 1; overflow: hidden; - padding: 4px 16px 64px; + padding: 4px 16px 50px; } .c0 { diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx index 435a210b9d260..86175c0e06ad2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx @@ -26,7 +26,8 @@ import { BrowserFields } from '../../../../common/containers/source'; import { EventDetails, EventsViewType, - View, + EventView, + ThreatView, } from '../../../../common/components/event_details/event_details'; import { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline'; import { LineClamp } from '../../../../common/components/line_clamp'; @@ -87,7 +88,8 @@ ExpandableEventTitle.displayName = 'ExpandableEventTitle'; export const ExpandableEvent = React.memo( ({ browserFields, event, timelineId, timelineTabType, isAlert, loading, detailsData }) => { - const [view, setView] = useState(EventsViewType.summaryView); + const [eventView, setEventView] = useState(EventsViewType.summaryView); + const [threatView, setThreatView] = useState(EventsViewType.threatSummaryView); const message = useMemo(() => { if (detailsData) { @@ -131,10 +133,12 @@ export const ExpandableEvent = React.memo( data={detailsData!} id={event.eventId!} isAlert={isAlert} - onViewSelected={setView} - timelineTabType={timelineTabType} + onThreatViewSelected={setThreatView} + onEventViewSelected={setEventView} + threatView={threatView} timelineId={timelineId} - view={view} + timelineTabType={timelineTabType} + eventView={eventView} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx index 6f4778f36466b..9a4684193b997 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx @@ -25,7 +25,7 @@ const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` .euiFlyoutBody__overflowContent { flex: 1; overflow: hidden; - padding: ${({ theme }) => `${theme.eui.paddingSizes.xs} ${theme.eui.paddingSizes.m} 64px`}; + padding: ${({ theme }) => `${theme.eui.paddingSizes.xs} ${theme.eui.paddingSizes.m} 50px`}; } } `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx index 3032f556251f3..e227c87b99870 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx @@ -44,7 +44,7 @@ const FormattedFieldValueComponent: React.FC<{ isObjectArray?: boolean; fieldFormat?: string; fieldName: string; - fieldType: string; + fieldType?: string; truncate?: boolean; value: string | number | undefined | null; linkValue?: string | null | undefined; diff --git a/x-pack/plugins/security_solution/server/search_strategy/helpers/format_response_object_values.ts b/x-pack/plugins/security_solution/server/search_strategy/helpers/format_response_object_values.ts index 4dab0ebc43149..0b418c0da410c 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/helpers/format_response_object_values.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/helpers/format_response_object_values.ts @@ -7,7 +7,7 @@ import { mapValues, isObject, isArray } from 'lodash/fp'; -import { toArray } from './to_array'; +import { toArray } from '../../../common/utils/to_array'; export const mapObjectValuesToStringArray = (object: object): object => mapValues((o) => { diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts index 3f4eb5721164b..bed4a040f92b0 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts @@ -14,8 +14,7 @@ import { HostsEdges, HostValue, } from '../../../../../../common/search_strategy/security_solution/hosts'; - -import { toObjectArrayOfStrings } from '../../../../helpers/to_array'; +import { toObjectArrayOfStrings } from '../../../../../../common/utils/to_array'; export const HOSTS_FIELDS: readonly string[] = [ '_id', diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts index aeaefe690cbde..807b78cb9cdd2 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts @@ -8,7 +8,7 @@ import { get, getOr, isEmpty } from 'lodash/fp'; import { set } from '@elastic/safer-lodash-set/fp'; import { mergeFieldsWithHit } from '../../../../../utils/build_query'; -import { toObjectArrayOfStrings } from '../../../../helpers/to_array'; +import { toObjectArrayOfStrings } from '../../../../../../common/utils/to_array'; import { AuthenticationsEdges, AuthenticationHit, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts index d36af61957690..00ed5c0c0dc01 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts @@ -8,6 +8,7 @@ import { set } from '@elastic/safer-lodash-set/fp'; import { get, has, head } from 'lodash/fp'; import { hostFieldsMap } from '../../../../../../common/ecs/ecs_fields'; +import { toObjectArrayOfStrings } from '../../../../../../common/utils/to_array'; import { Direction } from '../../../../../../common/search_strategy/common'; import { AggregationRequest, @@ -16,7 +17,6 @@ import { HostItem, HostValue, } from '../../../../../../common/search_strategy/security_solution/hosts'; -import { toObjectArrayOfStrings } from '../../../../helpers/to_array'; export const HOST_FIELDS = [ '_id', diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts index fe202b48540d7..1c1e2111f3771 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts @@ -14,7 +14,7 @@ import { HostsUncommonProcessesEdges, HostsUncommonProcessHit, } from '../../../../../../common/search_strategy/security_solution/hosts/uncommon_processes'; -import { toObjectArrayOfStrings } from '../../../../helpers/to_array'; +import { toObjectArrayOfStrings } from '../../../../../../common/utils/to_array'; import { HostHits } from '../../../../../../common/search_strategy'; export const uncommonProcessesFields = [ diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/helpers.ts index 8fc7ae0304a35..cc1bfdff8e096 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/helpers.ts @@ -13,7 +13,7 @@ import { NetworkDetailsHostHit, NetworkHit, } from '../../../../../../common/search_strategy/security_solution/network'; -import { toObjectArrayOfStrings } from '../../../../helpers/to_array'; +import { toObjectArrayOfStrings } from '../../../../../../common/utils/to_array'; export const getNetworkDetailsAgg = (type: string, networkHit: NetworkHit | {}) => { const firstSeen = getOr(null, `firstSeen.value_as_string`, networkHit); diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts index 61af6a7664faa..405ddba137dae 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts @@ -8,7 +8,7 @@ import { EventHit } from '../../../../../../common/search_strategy'; import { TIMELINE_EVENTS_FIELDS } from './constants'; import { formatTimelineData } from './helpers'; -import { eventHit } from '../mocks'; +import { eventHit } from '../../../../../../common/utils/mock_event_details'; describe('#formatTimelineData', () => { it('happy path', async () => { diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts index e5bb8cb7e14b7..2c18fb2840865 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts @@ -11,8 +11,11 @@ import { TimelineEdges, TimelineNonEcsData, } from '../../../../../../common/search_strategy'; -import { toStringArray } from '../../../../helpers/to_array'; -import { getDataSafety, getDataFromFieldsHits } from '../details/helpers'; +import { toStringArray } from '../../../../../../common/utils/to_array'; +import { + getDataFromFieldsHits, + getDataSafety, +} from '../../../../../../common/utils/field_formatters'; const getTimestamp = (hit: EventHit): string => { if (hit.fields && hit.fields['@timestamp']) { diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts index 0107ba44baec7..a4d6eebfb71b8 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts @@ -19,7 +19,11 @@ import { import { inspectStringifyObject } from '../../../../../utils/build_query'; import { SecuritySolutionTimelineFactory } from '../../types'; import { buildTimelineDetailsQuery } from './query.events_details.dsl'; -import { getDataFromFieldsHits, getDataFromSourceHits, getDataSafety } from './helpers'; +import { + getDataFromFieldsHits, + getDataFromSourceHits, + getDataSafety, +} from '../../../../../../common/utils/field_formatters'; export const timelineEventsDetails: SecuritySolutionTimelineFactory = { buildDsl: (options: TimelineEventsDetailsRequestOptions) => {