Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 12 commits into from
Apr 19, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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)`
Expand Down Expand Up @@ -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,
Expand All @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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__';
Expand All @@ -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 = {
Expand Down Expand Up @@ -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();
});
});
Expand All @@ -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');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,42 +6,43 @@
*/

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 { useThreatIntelTabs } from '../../hooks/use_threat_intel_tabs';

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 {
browserFields: BrowserFields;
data: TimelineEventsDetailsItem[];
id: string;
isAlert: boolean;
eventView: EventView;
threatView: ThreatView;
onEventViewSelected: (selected: EventView) => void;
onThreatViewSelected: (selected: ThreatView) => void;
timelineTabType: TimelineTabs | 'flyout';
timelineId: string;
}
Expand Down Expand Up @@ -77,132 +78,115 @@ 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 [selectedTabId, setSelectedTabId] = useState<EventViewId>(EventsViewType.summaryView);
const handleTabClick = useCallback(
(tab: EuiTabbedContentTab) => setSelectedTabId(tab.id as EventViewId),
[setSelectedTabId]
);
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 { isThreatPresent, threatCount, threatSummaryRows, threatDetailsRows } = useThreatIntelTabs(
data,
isAlert,
id,
timelineId,
selectedTabId
);

const selectedEventTab = useMemo(() => tabs.find((t) => t.id === eventView) ?? tabs[0], [
tabs,
eventView,
]);
const summaryTab = useMemo(
() =>
isAlert
? {
id: EventsViewType.summaryView,
name: i18n.SUMMARY,
content: (
<>
<AlertSummaryView
{...{
data,
eventId: id,
browserFields,
timelineId,
title: isThreatPresent ? i18n.ALERT_SUMMARY : undefined,
}}
/>
{isThreatPresent && <ThreatSummaryView threatSummaryRows={threatSummaryRows} />}
</>
),
}
: undefined,
[browserFields, data, id, isAlert, isThreatPresent, timelineId, threatSummaryRows]
);

const isThreatPresent: boolean = useMemo(
const threatIntelTab = useMemo(
() =>
selectedEventTab.id === tabs[0].id &&
isAlert &&
data.some((item) => item.field === INDICATOR_DESTINATION_PATH),
[tabs, selectedEventTab, isAlert, data]
isAlert
? {
id: EventsViewType.threatIntelView,
name: `${i18n.THREAT_INTEL} (${threatCount})`,
content: <ThreatDetailsView threatDetailsRows={threatDetailsRows} />,
}
: undefined,
[isAlert, threatDetailsRows, 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 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) => !!tab) as EventViewTab[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: if you defined this filter function as a type guard:

const isTab = (tab: EventViewTab | undefined): tab is EventViewTab => !!tab;

Then you could remove the as here.

}, [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"
/>
);
};

Expand Down
Loading