[CTI] Adds Threat Intel Tab to Alert Summary Flyout (#97185)

This commit is contained in:
Ece Özalp 2021-04-19 17:03:56 -04:00 committed by GitHub
parent b5effc20a4
commit a254f0f810
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 503 additions and 281 deletions

View file

@ -19,10 +19,14 @@ export const INDICATOR_MATCHED_TYPE = `${INDICATOR_DESTINATION_PATH}.${MATCHED_T
export const EVENT_DATASET = 'event.dataset';
export const EVENT_REFERENCE = 'event.reference';
export const PROVIDER = 'provider';
export const FIRSTSEEN = 'first_seen';
export const INDICATOR_DATASET = `${INDICATOR_DESTINATION_PATH}.${EVENT_DATASET}`;
export const INDICATOR_REFERENCE = `${INDICATOR_DESTINATION_PATH}.${EVENT_REFERENCE}`;
export const INDICATOR_EVENT_URL = `${INDICATOR_DESTINATION_PATH}.event.url`;
export const INDICATOR_FIRSTSEEN = `${INDICATOR_DESTINATION_PATH}.${FIRSTSEEN}`;
export const INDICATOR_LASTSEEN = `${INDICATOR_DESTINATION_PATH}.last_seen`;
export const INDICATOR_PROVIDER = `${INDICATOR_DESTINATION_PATH}.${PROVIDER}`;
export const INDICATOR_REFERENCE = `${INDICATOR_DESTINATION_PATH}.${EVENT_REFERENCE}`;
export const CTI_ROW_RENDERER_FIELDS = [
INDICATOR_MATCHED_ATOMIC,
@ -32,3 +36,11 @@ export const CTI_ROW_RENDERER_FIELDS = [
INDICATOR_REFERENCE,
INDICATOR_PROVIDER,
];
export const SORTED_THREAT_SUMMARY_FIELDS = [
INDICATOR_MATCHED_FIELD,
INDICATOR_MATCHED_TYPE,
INDICATOR_PROVIDER,
INDICATOR_FIRSTSEEN,
INDICATOR_LASTSEEN,
];

View file

@ -10,10 +10,13 @@ import {
EuiDescriptionList,
EuiDescriptionListDescription,
EuiDescriptionListTitle,
EuiSpacer,
} from '@elastic/eui';
import { get, getOr } from 'lodash/fp';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import * as i18n from './translations';
import { FormattedFieldValue } from '../../../timelines/components/timeline/body/renderers/formatted_field';
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy';
import { BrowserFields } from '../../../../common/search_strategy/index_fields';
@ -33,7 +36,6 @@ import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../../../networ
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)`
@ -166,7 +168,8 @@ const AlertSummaryViewComponent: React.FC<{
data: TimelineEventsDetailsItem[];
eventId: string;
timelineId: string;
}> = ({ browserFields, data, eventId, timelineId }) => {
title?: string;
}> = ({ browserFields, data, eventId, timelineId, title }) => {
const summaryRows = useMemo(() => getSummaryRows({ browserFields, data, eventId, timelineId }), [
browserFields,
data,
@ -184,7 +187,8 @@ const AlertSummaryViewComponent: React.FC<{
return (
<>
<SummaryView summaryColumns={summaryColumns} summaryRows={summaryRows} />
<EuiSpacer size="l" />
<SummaryView summaryColumns={summaryColumns} summaryRows={summaryRows} title={title} />
{maybeRule?.note && (
<StyledEuiDescriptionList data-test-subj={`summary-view-guide`} compressed>
<EuiDescriptionListTitle>{i18n.INVESTIGATION_GUIDE}</EuiDescriptionListTitle>

View file

@ -0,0 +1,52 @@
/*
* 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 { ThemeProvider } from 'styled-components';
import { useMountAppended } from '../../utils/use_mount_appended';
import { getMockTheme } from '../../lib/kibana/kibana_react.mock';
import { EmptyThreatDetailsView } from './empty_threat_details_view';
jest.mock('../../lib/kibana');
describe('EmptyThreatDetailsView', () => {
const mount = useMountAppended();
const mockTheme = getMockTheme({
eui: {
euiBreakpoints: {
l: '1200px',
},
paddingSizes: {
m: '8px',
xl: '32px',
},
},
});
beforeEach(() => {
jest.clearAllMocks();
});
test('renders correct items', () => {
const wrapper = mount(
<ThemeProvider theme={mockTheme}>
<EmptyThreatDetailsView />
</ThemeProvider>
);
expect(wrapper.find('[data-test-subj="empty-threat-details-view"]').exists()).toEqual(true);
});
test('renders link to docs', () => {
const wrapper = mount(
<ThemeProvider theme={mockTheme}>
<EmptyThreatDetailsView />
</ThemeProvider>
);
expect(wrapper.find('a').exists()).toEqual(true);
});
});

View file

@ -0,0 +1,50 @@
/*
* 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 { EuiLink, EuiSpacer, EuiTitle } from '@elastic/eui';
import React from 'react';
import styled from 'styled-components';
import * as i18n from './translations';
import { useKibana } from '../../lib/kibana';
const EmptyThreatDetailsViewContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
`;
const Span = styled.span`
color: ${({ theme }) => theme.eui.euiColorDarkShade};
line-height: 1.8em;
text-align: center;
padding: ${({ theme }) => `${theme.eui.paddingSizes.m} ${theme.eui.paddingSizes.xl}`};
`;
const EmptyThreatDetailsViewComponent: React.FC<{}> = () => {
const threatIntelDocsUrl = `${
useKibana().services.docLinks.links.filebeat.base
}/filebeat-module-threatintel.html`;
return (
<EmptyThreatDetailsViewContainer data-test-subj="empty-threat-details-view">
<EuiSpacer size="xxl" />
<EuiTitle size="m">
<h2>{i18n.NO_ENRICHMENT_FOUND}</h2>
</EuiTitle>
<Span>
{i18n.IF_CTI_NOT_ENABLED}
<EuiLink href={threatIntelDocsUrl} target="_blank">
{i18n.CHECK_DOCS}
</EuiLink>
</Span>
</EmptyThreatDetailsViewContainer>
);
};
EmptyThreatDetailsViewComponent.displayName = 'EmptyThreatDetailsView';
export const EmptyThreatDetailsView = React.memo(EmptyThreatDetailsViewComponent);

View file

@ -13,7 +13,7 @@ import '../../mock/match_media';
import '../../mock/react_beautiful_dnd';
import { mockDetailItemData, mockDetailItemDataId, TestProviders } from '../../mock';
import { EventDetails, EventsViewType, EventView, ThreatView } from './event_details';
import { EventDetails, EventsViewType } from './event_details';
import { mockBrowserFields } from '../../containers/source/mock';
import { useMountAppended } from '../../utils/use_mount_appended';
import { mockAlertDetailsData } from './__mocks__';
@ -32,8 +32,7 @@ describe('EventDetails', () => {
onThreatViewSelected: jest.fn(),
timelineTabType: TimelineTabs.query,
timelineId: 'test',
eventView: EventsViewType.summaryView as EventView,
threatView: EventsViewType.threatSummaryView as ThreatView,
eventView: EventsViewType.summaryView,
};
const alertsProps = {
@ -78,13 +77,14 @@ describe('EventDetails', () => {
});
describe('alerts tabs', () => {
['Summary', 'Table', 'JSON View'].forEach((tab) => {
['Summary', 'Threat Intel', 'Table', 'JSON View'].forEach((tab) => {
test(`it renders the ${tab} tab`, () => {
const expectedCopy = tab === 'Threat Intel' ? `${tab} (1)` : tab;
expect(
alertsWrapper
.find('[data-test-subj="eventDetails"]')
.find('[role="tablist"]')
.containsMatchingElement(<span>{tab}</span>)
.containsMatchingElement(<span>{expectedCopy}</span>)
).toBeTruthy();
});
});
@ -99,27 +99,4 @@ 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(<span>{tab}</span>)
).toBeTruthy();
});
});
test('the Summary tab is selected by default', () => {
expect(
alertsWrapper
.find('[data-test-subj="threatDetails"]')
.find('.euiTab-isSelected')
.first()
.text()
).toEqual('Threat Summary');
});
});
});

View file

@ -6,31 +6,37 @@
*/
import { EuiTabbedContent, EuiTabbedContentTab, EuiSpacer } from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import styled from 'styled-components';
import { BrowserFields } from '../../containers/source';
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline';
import { EventFieldsBrowser } from './event_fields_browser';
import { JsonView } from './json_view';
import * as i18n from './translations';
import { AlertSummaryView } from './alert_summary_view';
import { ThreatSummaryView } from './threat_summary_view';
import { ThreatDetailsView } from './threat_details_view';
import * as i18n from './translations';
import { AlertSummaryView } from './alert_summary_view';
import { BrowserFields } from '../../containers/source';
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline';
import { TimelineTabs } from '../../../../common/types/timeline';
import { INDICATOR_DESTINATION_PATH } from '../../../../common/constants';
import { getDataFromSourceHits } from '../../../../common/utils/field_formatters';
export type EventView =
interface EventViewTab {
id: EventViewId;
name: string;
content: JSX.Element;
}
export type EventViewId =
| EventsViewType.tableView
| EventsViewType.jsonView
| EventsViewType.summaryView;
export type ThreatView = EventsViewType.threatSummaryView | EventsViewType.threatDetailsView;
| EventsViewType.summaryView
| EventsViewType.threatIntelView;
export enum EventsViewType {
tableView = 'table-view',
jsonView = 'json-view',
summaryView = 'summary-view',
threatSummaryView = 'threat-summary-view',
threatDetailsView = 'threat-details-view',
threatIntelView = 'threat-intel-view',
}
interface Props {
@ -38,10 +44,6 @@ interface Props {
data: TimelineEventsDetailsItem[];
id: string;
isAlert: boolean;
eventView: EventView;
threatView: ThreatView;
onEventViewSelected: (selected: EventView) => void;
onThreatViewSelected: (selected: ThreatView) => void;
timelineTabType: TimelineTabs | 'flyout';
timelineId: string;
}
@ -56,7 +58,8 @@ const StyledEuiTabbedContent = styled(EuiTabbedContent)`
display: flex;
flex: 1;
flex-direction: column;
overflow: scroll;
overflow: hidden;
overflow-y: auto;
::-webkit-scrollbar {
-webkit-appearance: none;
width: 7px;
@ -77,132 +80,125 @@ const TabContentWrapper = styled.div`
const EventDetailsComponent: React.FC<Props> = ({
browserFields,
data,
eventView,
id,
isAlert,
onEventViewSelected,
onThreatViewSelected,
threatView,
timelineId,
timelineTabType,
}) => {
const handleEventTabClick = useCallback((e) => onEventViewSelected(e.id), [onEventViewSelected]);
const handleThreatTabClick = useCallback((e) => onThreatViewSelected(e.id), [
onThreatViewSelected,
]);
const alerts = useMemo(
() => [
{
id: EventsViewType.summaryView,
name: i18n.SUMMARY,
content: (
<>
<EuiSpacer size="l" />
<AlertSummaryView
{...{
data,
eventId: id,
browserFields,
timelineId,
}}
/>
</>
),
},
],
[data, id, browserFields, timelineId]
);
const tabs: EuiTabbedContentTab[] = useMemo(
() => [
...(isAlert ? alerts : []),
{
id: EventsViewType.tableView,
name: i18n.TABLE,
content: (
<>
<EuiSpacer size="l" />
<EventFieldsBrowser
browserFields={browserFields}
data={data}
eventId={id}
timelineId={timelineId}
timelineTabType={timelineTabType}
/>
</>
),
},
{
id: EventsViewType.jsonView,
'data-test-subj': 'jsonViewTab',
name: i18n.JSON_VIEW,
content: (
<>
<EuiSpacer size="m" />
<TabContentWrapper>
<JsonView data={data} />
</TabContentWrapper>
</>
),
},
],
[alerts, browserFields, data, id, isAlert, timelineId, timelineTabType]
const [selectedTabId, setSelectedTabId] = useState<EventViewId>(EventsViewType.summaryView);
const handleTabClick = useCallback(
(tab: EuiTabbedContentTab) => setSelectedTabId(tab.id as EventViewId),
[setSelectedTabId]
);
const selectedEventTab = useMemo(() => tabs.find((t) => t.id === eventView) ?? tabs[0], [
tabs,
eventView,
]);
const threatData = useMemo(() => {
if (isAlert && data) {
const threatIndicator = data.find(
({ field, originalValue }) => field === INDICATOR_DESTINATION_PATH && originalValue
);
if (!threatIndicator) return [];
const { originalValue } = threatIndicator;
const values = Array.isArray(originalValue) ? originalValue : [originalValue];
return values.map((value) => getDataFromSourceHits(JSON.parse(value)));
}
return [];
}, [data, isAlert]);
const isThreatPresent: boolean = useMemo(
const threatCount = useMemo(() => threatData.length, [threatData.length]);
const summaryTab = useMemo(
() =>
selectedEventTab.id === tabs[0].id &&
isAlert &&
data.some((item) => item.field === INDICATOR_DESTINATION_PATH),
[tabs, selectedEventTab, isAlert, data]
isAlert
? {
id: EventsViewType.summaryView,
name: i18n.SUMMARY,
content: (
<>
<AlertSummaryView
{...{
data,
eventId: id,
browserFields,
timelineId,
title: threatCount ? i18n.ALERT_SUMMARY : undefined,
}}
/>
{threatCount > 0 && <ThreatSummaryView {...{ data, timelineId, eventId: id }} />}
</>
),
}
: undefined,
[browserFields, data, id, isAlert, timelineId, threatCount]
);
const threatTabs: EuiTabbedContentTab[] = useMemo(() => {
return isAlert && isThreatPresent
? [
{
id: EventsViewType.threatSummaryView,
name: i18n.THREAT_SUMMARY,
content: <ThreatSummaryView {...{ data, eventId: id, timelineId }} />,
},
{
id: EventsViewType.threatDetailsView,
name: i18n.THREAT_DETAILS,
content: <ThreatDetailsView data={data} />,
},
]
: [];
}, [data, id, isAlert, timelineId, isThreatPresent]);
const selectedThreatTab = useMemo(
() => threatTabs.find((t) => t.id === threatView) ?? threatTabs[0],
[threatTabs, threatView]
const threatIntelTab = useMemo(
() =>
isAlert
? {
id: EventsViewType.threatIntelView,
name: `${i18n.THREAT_INTEL} (${threatCount})`,
content: <ThreatDetailsView threatData={threatData} />,
}
: undefined,
[isAlert, threatCount, threatData]
);
const tableTab = useMemo(
() => ({
id: EventsViewType.tableView,
name: i18n.TABLE,
content: (
<>
<EuiSpacer size="l" />
<EventFieldsBrowser
browserFields={browserFields}
data={data}
eventId={id}
timelineId={timelineId}
timelineTabType={timelineTabType}
/>
</>
),
}),
[browserFields, data, id, timelineId, timelineTabType]
);
const jsonTab = useMemo(
() => ({
id: EventsViewType.jsonView,
'data-test-subj': 'jsonViewTab',
name: i18n.JSON_VIEW,
content: (
<>
<EuiSpacer size="m" />
<TabContentWrapper>
<JsonView data={data} />
</TabContentWrapper>
</>
),
}),
[data]
);
const tabs = useMemo(() => {
return [summaryTab, threatIntelTab, tableTab, jsonTab].filter(
(tab: EventViewTab | undefined): tab is EventViewTab => !!tab
);
}, [summaryTab, threatIntelTab, tableTab, jsonTab]);
const selectedTab = useMemo(() => tabs.find((tab) => tab.id === selectedTabId), [
tabs,
selectedTabId,
]);
return (
<>
<StyledEuiTabbedContent
data-test-subj="eventDetails"
tabs={tabs}
selectedTab={selectedEventTab}
onTabClick={handleEventTabClick}
key="event-summary-tabs"
/>
{isThreatPresent && (
<StyledEuiTabbedContent
data-test-subj="threatDetails"
tabs={threatTabs}
selectedTab={selectedThreatTab}
onTabClick={handleThreatTabClick}
key="threat-summary-tabs"
/>
)}
</>
<StyledEuiTabbedContent
data-test-subj="eventDetails"
tabs={tabs}
selectedTab={selectedTab}
onTabClick={handleTabClick}
key="event-summary-tabs"
/>
);
};

View file

@ -225,7 +225,7 @@ export const getSummaryColumns = (
field: 'title',
truncateText: false,
render: getTitle,
width: '120px',
width: '160px',
name: '',
},
{

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui';
import { EuiInMemoryTable, EuiBasicTableColumn, EuiTitle, EuiHorizontalRule } from '@elastic/eui';
import React from 'react';
import styled from 'styled-components';
@ -27,18 +27,47 @@ const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)`
}
`;
const StyledEuiTitle = styled(EuiTitle)`
color: ${({ theme }) => theme.eui.euiColorDarkShade};
text-transform: lowercase;
padding-top: ${({ theme }) => theme.eui.paddingSizes.s};
h2 {
min-width: 120px;
}
hr {
max-width: 75%;
}
`;
const FlexDiv = styled.div`
display: flex;
align-items: center;
justify-content: flex-start;
`;
export const SummaryViewComponent: React.FC<{
title?: string;
summaryColumns: Array<EuiBasicTableColumn<SummaryRow>>;
summaryRows: SummaryRow[];
dataTestSubj?: string;
}> = ({ summaryColumns, summaryRows, dataTestSubj = 'summary-view' }) => {
}> = ({ summaryColumns, summaryRows, dataTestSubj = 'summary-view', title }) => {
return (
<StyledEuiInMemoryTable
data-test-subj={dataTestSubj}
items={summaryRows}
columns={summaryColumns}
compressed
/>
<>
{title && (
<StyledEuiTitle size="xxs">
<FlexDiv>
<h2>{title}</h2>
<EuiHorizontalRule margin="none" />
</FlexDiv>
</StyledEuiTitle>
)}
<StyledEuiInMemoryTable
data-test-subj={dataTestSubj}
items={summaryRows}
columns={summaryColumns}
compressed
/>
</>
);
};

View file

@ -8,8 +8,6 @@
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';
@ -20,11 +18,56 @@ jest.mock('../../../detections/containers/detection_engine/rules/use_rule_async'
};
});
const props = {
data: mockAlertDetailsData as TimelineEventsDetailsItem[],
eventId: '5d1d53da502f56aacc14c3cb5c669363d102b31f99822e5d369d4804ed370a31',
timelineId: 'detections-page',
};
const mostRecentDate = '2021-04-25T18:17:00.000Z';
const threatData = [
[
{
category: 'matched',
field: 'matched.field',
isObjectArray: false,
originalValue: ['test_field_2'],
values: ['test_field_2'],
},
{
category: 'first_seen',
field: 'first_seen',
isObjectArray: false,
originalValue: ['2019-04-25T18:17:00.000Z'],
values: ['2019-04-25T18:17:00.000Z'],
},
{
category: 'event',
field: 'event.reference',
isObjectArray: false,
originalValue: ['https://test.com/'],
values: ['https://test.com/'],
},
{
category: 'event',
field: 'event.url',
isObjectArray: false,
originalValue: ['https://test2.com/'],
values: ['https://test2.com/'],
},
],
[
{
category: 'first_seen',
field: 'first_seen',
isObjectArray: false,
originalValue: [mostRecentDate],
values: [mostRecentDate],
},
{
category: 'matched',
field: 'matched.field',
isObjectArray: false,
originalValue: ['test_field'],
values: ['test_field'],
},
],
];
describe('ThreatDetailsView', () => {
const mount = useMountAppended();
@ -36,9 +79,36 @@ describe('ThreatDetailsView', () => {
test('render correct items', () => {
const wrapper = mount(
<TestProviders>
<ThreatDetailsView {...props} />
<ThreatDetailsView threatData={threatData} />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="threat-details-view-0"]').exists()).toEqual(true);
});
test('renders empty view if there are no items', () => {
const wrapper = mount(
<TestProviders>
<ThreatDetailsView threatData={[[]]} />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="empty-threat-details-view"]').exists()).toEqual(true);
});
test('renders link for event.url and event.reference', () => {
const wrapper = mount(
<TestProviders>
<ThreatDetailsView threatData={threatData} />
</TestProviders>
);
expect(wrapper.find('a').length).toEqual(2);
});
test('orders items by first_seen', () => {
const wrapper = mount(
<TestProviders>
<ThreatDetailsView threatData={threatData} />
</TestProviders>
);
expect(wrapper.find('.euiToolTipAnchor span').at(0).text()).toEqual(mostRecentDate);
});
});

View file

@ -10,51 +10,50 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiSpacer,
EuiToolTip,
EuiLink,
} from '@elastic/eui';
import React, { useMemo } from 'react';
import React from 'react';
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy';
import { isEmpty } from 'fp-ts/Array';
import { SummaryView } from './summary_view';
import { getSummaryColumns, SummaryRow, ThreatDetailsRow } from './helpers';
import { getDataFromSourceHits } from '../../../../common/utils/field_formatters';
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy';
import { INDICATOR_DESTINATION_PATH } from '../../../../common/constants';
import {
FIRSTSEEN,
INDICATOR_EVENT_URL,
INDICATOR_REFERENCE,
} from '../../../../common/cti/constants';
import { EmptyThreatDetailsView } from './empty_threat_details_view';
const ThreatDetailsDescription: React.FC<ThreatDetailsRow['description']> = ({
fieldName,
value,
}) => (
<EuiToolTip
data-test-subj="message-tool-tip"
content={
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem grow={false}>
<span>{fieldName}</span>
</EuiFlexItem>
</EuiFlexGroup>
}
>
}) => {
const tooltipChild = [INDICATOR_EVENT_URL, INDICATOR_REFERENCE].some(
(field) => field === fieldName
) ? (
<EuiLink href={value} target="_blank">
{value}
</EuiLink>
) : (
<span>{value}</span>
</EuiToolTip>
);
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 },
}))
return (
<EuiToolTip
data-test-subj="message-tool-tip"
content={
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem grow={false}>
<span>{fieldName}</span>
</EuiFlexItem>
</EuiFlexGroup>
}
>
{tooltipChild}
</EuiToolTip>
);
};
@ -62,17 +61,51 @@ const summaryColumns: Array<EuiBasicTableColumn<SummaryRow>> = getSummaryColumns
ThreatDetailsDescription
);
const getISOStringFromThreatDataItem = (threatDataItem: TimelineEventsDetailsItem[]) => {
const firstSeen = threatDataItem.find(
(item: TimelineEventsDetailsItem) => item.field === FIRSTSEEN
);
if (firstSeen) {
const { originalValue } = firstSeen;
const firstSeenValue = Array.isArray(originalValue) ? originalValue[0] : originalValue;
if (!Number.isNaN(Date.parse(firstSeenValue))) {
return firstSeenValue;
}
}
return new Date(-1).toString();
};
const getThreatDetailsRowsArray = (threatData: TimelineEventsDetailsItem[][]) =>
threatData
.sort(
(a, b) =>
Date.parse(getISOStringFromThreatDataItem(b)) -
Date.parse(getISOStringFromThreatDataItem(a))
)
.map((items) =>
items.map(({ field, originalValue }) => ({
title: field,
description: {
fieldName: `${INDICATOR_DESTINATION_PATH}.${field}`,
value: Array.isArray(originalValue) ? originalValue[0] : originalValue,
},
}))
);
const ThreatDetailsViewComponent: React.FC<{
data: TimelineEventsDetailsItem[];
}> = ({ data }) => {
const summaryRowsArray = useMemo(() => getSummaryRowsArray({ data }), [data]);
return (
threatData: TimelineEventsDetailsItem[][];
}> = ({ threatData }) => {
const threatDetailsRowsArray = getThreatDetailsRowsArray(threatData);
return isEmpty(threatDetailsRowsArray) || isEmpty(threatDetailsRowsArray[0]) ? (
<EmptyThreatDetailsView />
) : (
<>
{summaryRowsArray.map((summaryRows, index, arr) => {
{threatDetailsRowsArray.map((summaryRows, index, arr) => {
const key = summaryRows.find((threat) => threat.title === 'matched.id')?.description
.value[0];
return (
<div key={key}>
<div key={`${key}-${index}`}>
{index === 0 && <EuiSpacer size="l" />}
<SummaryView
summaryColumns={summaryColumns}
summaryRows={summaryRows}

View file

@ -8,11 +8,10 @@
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';
import { mockAlertDetailsData } from './__mocks__';
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy';
jest.mock('../../../detections/containers/detection_engine/rules/use_rule_async', () => {
return {

View file

@ -5,16 +5,39 @@
* 2.0.
*/
import { EuiBasicTableColumn } from '@elastic/eui';
import React, { useMemo } from 'react';
import { EuiBasicTableColumn, EuiSpacer } from '@elastic/eui';
import React 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 * as i18n from './translations';
import { SummaryView } from './summary_view';
import { getSummaryColumns, SummaryRow, ThreatSummaryRow } from './helpers';
import { FormattedFieldValue } from '../../../timelines/components/timeline/body/renderers/formatted_field';
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline';
import { SORTED_THREAT_SUMMARY_FIELDS } from '../../../../common/cti/constants';
import { INDICATOR_DESTINATION_PATH } from '../../../../common/constants';
const getThreatSummaryRows = (
data: TimelineEventsDetailsItem[],
timelineId: string,
eventId: string
) =>
SORTED_THREAT_SUMMARY_FIELDS.map((threatSummaryField) => {
const item = data.find(({ field }) => field === threatSummaryField);
if (item) {
const { field, originalValue } = item;
return {
title: field.replace(`${INDICATOR_DESTINATION_PATH}.`, ''),
description: {
values: Array.isArray(originalValue) ? originalValue : [originalValue],
contextId: timelineId,
eventId,
fieldName: field,
},
};
}
return null;
}).filter((item: ThreatSummaryRow | null): item is ThreatSummaryRow => !!item);
const getDescription = ({
contextId,
eventId,
@ -34,56 +57,22 @@ const getDescription = ({
</>
);
const getSummaryRows = ({
data,
timelineId: contextId,
eventId,
}: {
data: TimelineEventsDetailsItem[];
browserFields?: BrowserFields;
timelineId: string;
eventId: string;
}) => {
if (!data) return [];
return data.reduce<SummaryRow[]>((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<EuiBasicTableColumn<SummaryRow>> = 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 (
eventId: string;
}> = ({ data, timelineId, eventId }) => (
<>
<EuiSpacer size="l" />
<SummaryView
title={i18n.THREAT_SUMMARY}
summaryColumns={summaryColumns}
summaryRows={summaryRows}
summaryRows={getThreatSummaryRows(data, timelineId, eventId)}
dataTestSubj="threat-summary-view"
/>
);
};
</>
);
export const ThreatSummaryView = React.memo(ThreatSummaryViewComponent);

View file

@ -11,12 +11,35 @@ export const SUMMARY = i18n.translate('xpack.securitySolution.alertDetails.summa
defaultMessage: 'Summary',
});
export const ALERT_SUMMARY = i18n.translate('xpack.securitySolution.alertDetails.alertSummary', {
defaultMessage: 'Alert Summary',
});
export const THREAT_INTEL = i18n.translate('xpack.securitySolution.alertDetails.threatIntel', {
defaultMessage: 'Threat Intel',
});
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 NO_ENRICHMENT_FOUND = i18n.translate(
'xpack.securitySolution.alertDetails.noEnrichmentFound',
{
defaultMessage: 'No Threat Intel Enrichment Found',
}
);
export const IF_CTI_NOT_ENABLED = i18n.translate(
'xpack.securitySolution.alertDetails.ifCtiNotEnabled',
{
defaultMessage:
"If you haven't enabled any threat intelligence sources and want to learn more about this capability, ",
}
);
export const CHECK_DOCS = i18n.translate('xpack.securitySolution.alertDetails.checkDocs', {
defaultMessage: 'please check out our documentation.',
});
export const INVESTIGATION_GUIDE = i18n.translate(

View file

@ -18,17 +18,12 @@ import {
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import React, { useMemo, useState } from 'react';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import { TimelineTabs } from '../../../../../common/types/timeline';
import { BrowserFields } from '../../../../common/containers/source';
import {
EventDetails,
EventsViewType,
EventView,
ThreatView,
} from '../../../../common/components/event_details/event_details';
import { EventDetails } from '../../../../common/components/event_details/event_details';
import { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline';
import { LineClamp } from '../../../../common/components/line_clamp';
import * as i18n from './translations';
@ -88,9 +83,6 @@ ExpandableEventTitle.displayName = 'ExpandableEventTitle';
export const ExpandableEvent = React.memo<Props>(
({ browserFields, event, timelineId, timelineTabType, isAlert, loading, detailsData }) => {
const [eventView, setEventView] = useState<EventView>(EventsViewType.summaryView);
const [threatView, setThreatView] = useState<ThreatView>(EventsViewType.threatSummaryView);
const message = useMemo(() => {
if (detailsData) {
const messageField = find({ category: 'base', field: 'message' }, detailsData) as
@ -133,12 +125,8 @@ export const ExpandableEvent = React.memo<Props>(
data={detailsData!}
id={event.eventId!}
isAlert={isAlert}
onThreatViewSelected={setThreatView}
onEventViewSelected={setEventView}
threatView={threatView}
timelineId={timelineId}
timelineTabType={timelineTabType}
eventView={eventView}
/>
</StyledEuiFlexItem>
</StyledFlexGroup>