diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index a6d263c120a20..b98e5f3161034 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -92,6 +92,10 @@ export const allowedExperimentalValues = Object.freeze({ * Enables expandable flyout in create rule page, alert preview */ expandableFlyoutInCreateRuleEnabled: true, + /** + * Enables expandable flyout for event type documents + */ + expandableEventFlyoutEnabled: false, /* * Enables new Set of filters on the Alerts page. * diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/content.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/content.tsx index a8beaad3a9adb..fa8db3635be20 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/content.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/content.tsx @@ -10,7 +10,7 @@ import type { VFC } from 'react'; import React, { useMemo } from 'react'; import { css } from '@emotion/react'; import type { LeftPanelPaths } from '.'; -import { tabs } from './tabs'; +import type { LeftPanelTabType } from './tabs'; import { FlyoutBody } from '../../shared/components/flyout_body'; export interface PanelContentProps { @@ -18,16 +18,20 @@ export interface PanelContentProps { * Id of the tab selected in the parent component to display its content */ selectedTabId: LeftPanelPaths; + /** + * Tabs display at the top of left panel + */ + tabs: LeftPanelTabType[]; } /** * Document details expandable flyout left section. Appears after the user clicks on the expand details button in the right section. * Displays the content of investigation and insights tabs (visualize is hidden for 8.9). */ -export const PanelContent: VFC = ({ selectedTabId }) => { +export const PanelContent: VFC = ({ selectedTabId, tabs }) => { const selectedTabContent = useMemo(() => { - return tabs.filter((tab) => tab.visible).find((tab) => tab.id === selectedTabId)?.content; - }, [selectedTabId]); + return tabs.find((tab) => tab.id === selectedTabId)?.content; + }, [selectedTabId, tabs]); return ( void; + /** + * Tabs display at the top of left panel + */ + tabs: LeftPanelTabType[]; } /** * Header at the top of the left section. - * Displays the investigation and insights tabs (visualize is hidden for 8.9). + * Displays the insights, investigation and response tabs (visualize is hidden for 8.9+). */ -export const PanelHeader: VFC = memo(({ selectedTabId, setSelectedTabId }) => { - const onSelectedTabChanged = (id: LeftPanelPaths) => setSelectedTabId(id); - const renderTabs = tabs - .filter((tab) => tab.visible) - .map((tab, index) => ( +export const PanelHeader: VFC = memo( + ({ selectedTabId, setSelectedTabId, tabs }) => { + const { getFieldsData } = useLeftPanelContext(); + const isEventKindSignal = getField(getFieldsData('event.kind')) === EventKind.signal; + + const onSelectedTabChanged = (id: LeftPanelPaths) => setSelectedTabId(id); + const renderTabs = tabs.map((tab, index) => ( onSelectedTabChanged(tab.id)} isSelected={tab.id === selectedTabId} @@ -44,19 +53,20 @@ export const PanelHeader: VFC = memo(({ selectedTabId, setSele )); - return ( - - - {renderTabs} - - - ); -}); + return ( + + + {renderTabs} + + + ); + } +); PanelHeader.displayName = 'PanelHeader'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/index.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/index.tsx index 6d02ad847d847..cd1b3b0601498 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/index.tsx @@ -11,8 +11,10 @@ import type { FlyoutPanelProps, PanelPath } from '@kbn/expandable-flyout'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { PanelHeader } from './header'; import { PanelContent } from './content'; -import type { LeftPanelTabsType } from './tabs'; -import { tabs } from './tabs'; +import type { LeftPanelTabType } from './tabs'; +import * as tabs from './tabs'; +import { getField } from '../shared/utils'; +import { EventKind } from '../shared/constants/event_kinds'; import { useLeftPanelContext } from './context'; export type LeftPanelPaths = 'visualize' | 'insights' | 'investigation' | 'response'; @@ -34,16 +36,24 @@ export interface LeftPanelProps extends FlyoutPanelProps { export const LeftPanel: FC> = memo(({ path }) => { const { openLeftPanel } = useExpandableFlyoutApi(); - const { eventId, indexName, scopeId } = useLeftPanelContext(); + const { eventId, indexName, scopeId, getFieldsData } = useLeftPanelContext(); + const eventKind = getField(getFieldsData('event.kind')); + + const tabsDisplayed = useMemo( + () => + eventKind === EventKind.signal + ? [tabs.insightsTab, tabs.investigationTab, tabs.responseTab] + : [tabs.insightsTab], + [eventKind] + ); const selectedTabId = useMemo(() => { - const visibleTabs = tabs.filter((tab) => tab.visible); - const defaultTab = visibleTabs[0].id; + const defaultTab = tabsDisplayed[0].id; if (!path) return defaultTab; - return visibleTabs.map((tab) => tab.id).find((tabId) => tabId === path.tab) ?? defaultTab; - }, [path]); + return tabsDisplayed.map((tab) => tab.id).find((tabId) => tabId === path.tab) ?? defaultTab; + }, [path, tabsDisplayed]); - const setSelectedTabId = (tabId: LeftPanelTabsType[number]['id']) => { + const setSelectedTabId = (tabId: LeftPanelTabType['id']) => { openLeftPanel({ id: DocumentDetailsLeftPanelKey, path: { @@ -59,8 +69,12 @@ export const LeftPanel: FC> = memo(({ path }) => { return ( <> - - + + ); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs.tsx index c2fc6e5fe20db..4851f4a455959 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs.tsx @@ -20,61 +20,57 @@ import { } from './test_ids'; import { ResponseTab } from './tabs/response_tab'; -export type LeftPanelTabsType = Array<{ +export interface LeftPanelTabType { id: LeftPanelPaths; 'data-test-subj': string; name: ReactElement; content: React.ReactElement; - visible: boolean; -}>; +} -export const tabs: LeftPanelTabsType = [ - { - id: 'visualize', - 'data-test-subj': VISUALIZE_TAB_TEST_ID, - name: ( - - ), - content: , - visible: false, - }, - { - id: 'insights', - 'data-test-subj': INSIGHTS_TAB_TEST_ID, - name: ( - - ), - content: , - visible: true, - }, - { - id: 'investigation', - 'data-test-subj': INVESTIGATION_TAB_TEST_ID, - name: ( - - ), - content: , - visible: true, - }, - { - id: 'response', - 'data-test-subj': RESPONSE_TAB_TEST_ID, - name: ( - - ), - content: , - visible: true, - }, -]; +export const visualizeTab: LeftPanelTabType = { + id: 'visualize', + 'data-test-subj': VISUALIZE_TAB_TEST_ID, + name: ( + + ), + content: , +}; + +export const insightsTab: LeftPanelTabType = { + id: 'insights', + 'data-test-subj': INSIGHTS_TAB_TEST_ID, + name: ( + + ), + content: , +}; + +export const investigationTab: LeftPanelTabType = { + id: 'investigation', + 'data-test-subj': INVESTIGATION_TAB_TEST_ID, + name: ( + + ), + content: , +}; + +export const responseTab: LeftPanelTabType = { + id: 'response', + 'data-test-subj': RESPONSE_TAB_TEST_ID, + name: ( + + ), + content: , +}; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/insights_tab.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/insights_tab.tsx index 366131f5a5725..38db170073df8 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/insights_tab.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/insights_tab.tsx @@ -5,8 +5,7 @@ * 2.0. */ -import React, { memo, useCallback } from 'react'; - +import React, { memo, useCallback, useMemo } from 'react'; import { EuiButtonGroup, EuiSpacer } from '@elastic/eui'; import type { EuiButtonGroupOptionProps } from '@elastic/eui/src/components/button/button_group/button_group'; import { i18n } from '@kbn/i18n'; @@ -28,6 +27,8 @@ import { } from '../components/threat_intelligence_details'; import { PREVALENCE_TAB_ID, PrevalenceDetails } from '../components/prevalence_details'; import { CORRELATIONS_TAB_ID, CorrelationsDetails } from '../components/correlations_details'; +import { getField } from '../../shared/utils'; +import { EventKind } from '../../shared/constants/event_kinds'; const insightsButtons: EuiButtonGroupOptionProps[] = [ { @@ -76,11 +77,25 @@ const insightsButtons: EuiButtonGroupOptionProps[] = [ * Insights view displayed in the document details expandable flyout left section */ export const InsightsTab: React.FC = memo(() => { - const { eventId, indexName, scopeId } = useLeftPanelContext(); + const { eventId, indexName, scopeId, getFieldsData } = useLeftPanelContext(); + const isEventKindSignal = getField(getFieldsData('event.kind')) === EventKind.signal; const { openLeftPanel } = useExpandableFlyoutApi(); const panels = useExpandableFlyoutState(); const activeInsightsId = panels.left?.path?.subTab ?? ENTITIES_TAB_ID; + // insight tabs based on whether document is alert or non-alert + // alert: entities, threat intelligence, prevalence, correlations + // non-alert: entities, prevalence + const buttonGroup = useMemo( + () => + isEventKindSignal + ? insightsButtons + : insightsButtons.filter( + (tab) => tab.id === ENTITIES_TAB_ID || tab.id === PREVALENCE_TAB_ID + ), + [isEventKindSignal] + ); + const onChangeCompressed = useCallback( (optionId: string) => { openLeftPanel({ @@ -103,19 +118,17 @@ export const InsightsTab: React.FC = memo(() => { <> {activeInsightsId === ENTITIES_TAB_ID && } diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/about_section.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/about_section.test.tsx index 9559a696d1c83..0130dd3ca0afd 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/about_section.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/about_section.test.tsx @@ -7,7 +7,13 @@ import React from 'react'; import { act, render } from '@testing-library/react'; -import { ABOUT_SECTION_CONTENT_TEST_ID, ABOUT_SECTION_HEADER_TEST_ID } from './test_ids'; +import { + ABOUT_SECTION_CONTENT_TEST_ID, + ABOUT_SECTION_HEADER_TEST_ID, + ALERT_DESCRIPTION_TITLE_TEST_ID, + EVENT_KIND_DESCRIPTION_TEST_ID, + EVENT_CATEGORY_DESCRIPTION_TEST_ID, +} from './test_ids'; import { TestProviders } from '../../../../common/mock'; import { AboutSection } from './about_section'; import { RightPanelContext } from '../context'; @@ -15,14 +21,21 @@ import { mockContextValue } from '../mocks/mock_context'; jest.mock('../../../../common/components/link_to'); -const renderAboutSection = (expanded: boolean = false) => - render( +const renderAboutSection = (expanded: boolean = false) => { + const mockGetFieldsData = (field: string) => { + switch (field) { + case 'event.kind': + return 'signal'; + } + }; + return render( - + ); +}; describe('', () => { it('should render the component collapsed', async () => { @@ -47,4 +60,65 @@ describe('', () => { expect(getByTestId(ABOUT_SECTION_CONTENT_TEST_ID)).toBeInTheDocument(); }); }); + + it('should render about section for signal document', async () => { + const { getByTestId } = renderAboutSection(true); + await act(async () => { + expect(getByTestId(ALERT_DESCRIPTION_TITLE_TEST_ID)).toBeInTheDocument(); + }); + }); + + it('should render event kind description if event.kind is not event', async () => { + const mockGetFieldsData = (field: string) => { + switch (field) { + case 'event.kind': + return 'alert'; + case 'event.category': + return 'behavior'; + } + }; + const { getByTestId, queryByTestId } = render( + + + + + + ); + await act(async () => { + expect(queryByTestId(ALERT_DESCRIPTION_TITLE_TEST_ID)).not.toBeInTheDocument(); + expect(getByTestId(EVENT_KIND_DESCRIPTION_TEST_ID)).toBeInTheDocument(); + }); + }); + + it('should render event category description if event.kind is event', async () => { + const mockGetFieldsData = (field: string) => { + switch (field) { + case 'event.kind': + return 'event'; + case 'event.category': + return 'behavior'; + } + }; + const { getByTestId, queryByTestId } = render( + + + + + + ); + await act(async () => { + expect(queryByTestId(ALERT_DESCRIPTION_TITLE_TEST_ID)).not.toBeInTheDocument(); + expect(getByTestId(`${EVENT_CATEGORY_DESCRIPTION_TEST_ID}-behavior`)).toBeInTheDocument(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/about_section.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/about_section.tsx index 1752ff91c3f03..a91b04272c874 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/about_section.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/about_section.tsx @@ -5,15 +5,21 @@ * 2.0. */ -import { EuiSpacer } from '@elastic/eui'; import type { VFC } from 'react'; import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { ExpandableSection } from './expandable_section'; import { ABOUT_SECTION_TEST_ID } from './test_ids'; -import { Description } from './description'; +import { AlertDescription } from './alert_description'; import { Reason } from './reason'; import { MitreAttack } from './mitre_attack'; +import { getField } from '../../shared/utils'; +import { EventKind } from '../../shared/constants/event_kinds'; +import { useRightPanelContext } from '../context'; +import { isEcsAllowedValue } from '../utils/event_utils'; +import { EventCategoryDescription } from './event_category_description'; +import { EventKindDescription } from './event_kind_description'; +import { EventRenderer } from './event_renderer'; export interface AboutSectionProps { /** @@ -23,9 +29,36 @@ export interface AboutSectionProps { } /** - * Most top section of the overview tab. It contains the description, reason and mitre attack information (for a document of type alert). + * Most top section of the overview tab. + * For alerts (event.kind is signal), it contains the description, reason and mitre attack information. + * For generic events (event.kind is event), it shows the event category description and event renderer. + * For all other events, it shows the event kind description, a list of event categories and event renderer. */ export const AboutSection: VFC = ({ expanded = true }) => { + const { getFieldsData } = useRightPanelContext(); + const eventKind = getField(getFieldsData('event.kind')); + const eventKindInECS = eventKind && isEcsAllowedValue('event.kind', eventKind); + + if (eventKind === EventKind.signal) { + return ( + + } + data-test-subj={ABOUT_SECTION_TEST_ID} + gutterSize="s" + > + + + + + ); + } + return ( = ({ expanded = true }) => { /> } data-test-subj={ABOUT_SECTION_TEST_ID} + gutterSize="s" > - - - - + {eventKindInECS && + (eventKind === 'event' ? ( + // if event kind is event, show a detailed description based on event category + + ) : ( + // if event kind is not event, show a higher level description on event kind + + ))} + ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/description.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_description.test.tsx similarity index 87% rename from x-pack/plugins/security_solution/public/flyout/document_details/right/components/description.test.tsx rename to x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_description.test.tsx index 4a42a9bd8987e..97289751a123e 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/description.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_description.test.tsx @@ -9,11 +9,11 @@ import React from 'react'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { render } from '@testing-library/react'; import { - DESCRIPTION_TITLE_TEST_ID, + ALERT_DESCRIPTION_TITLE_TEST_ID, RULE_SUMMARY_BUTTON_TEST_ID, - DESCRIPTION_DETAILS_TEST_ID, + ALERT_DESCRIPTION_DETAILS_TEST_ID, } from './test_ids'; -import { Description } from './description'; +import { AlertDescription } from './alert_description'; import { RightPanelContext } from '../context'; import { mockGetFieldsData } from '../../shared/mocks/mock_get_fields_data'; import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; @@ -65,14 +65,14 @@ const renderDescription = (panelContext: RightPanelContext) => render( - + ); const NO_DATA_MESSAGE = "There's no description for this rule."; -describe('', () => { +describe('', () => { beforeAll(() => { jest.mocked(useExpandableFlyoutApi).mockReturnValue(flyoutContextValue); }); @@ -82,24 +82,26 @@ describe('', () => { panelContextValue([ruleUuid, ruleDescription, ruleName]) ); - expect(getByTestId(DESCRIPTION_TITLE_TEST_ID)).toBeInTheDocument(); - expect(getByTestId(DESCRIPTION_TITLE_TEST_ID)).toHaveTextContent('Rule description'); - expect(getByTestId(DESCRIPTION_DETAILS_TEST_ID)).toHaveTextContent(ruleDescription.values[0]); + expect(getByTestId(ALERT_DESCRIPTION_TITLE_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(ALERT_DESCRIPTION_TITLE_TEST_ID)).toHaveTextContent('Rule description'); + expect(getByTestId(ALERT_DESCRIPTION_DETAILS_TEST_ID)).toHaveTextContent( + ruleDescription.values[0] + ); expect(getByTestId(RULE_SUMMARY_BUTTON_TEST_ID)).toBeInTheDocument(); }); it('should render no data message if rule description is not available', () => { const { getByTestId, getByText } = renderDescription(panelContextValue([ruleUuid])); - expect(getByTestId(DESCRIPTION_DETAILS_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(ALERT_DESCRIPTION_DETAILS_TEST_ID)).toBeInTheDocument(); expect(getByText(NO_DATA_MESSAGE)).toBeInTheDocument(); }); it('should render document title if document is not an alert', () => { const { getByTestId } = renderDescription(panelContextValue([ruleDescription])); - expect(getByTestId(DESCRIPTION_TITLE_TEST_ID)).toBeInTheDocument(); - expect(getByTestId(DESCRIPTION_TITLE_TEST_ID)).toHaveTextContent('Document description'); + expect(getByTestId(ALERT_DESCRIPTION_TITLE_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(ALERT_DESCRIPTION_TITLE_TEST_ID)).toHaveTextContent('Document description'); }); describe('rule preview', () => { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/description.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_description.tsx similarity index 91% rename from x-pack/plugins/security_solution/public/flyout/document_details/right/components/description.tsx rename to x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_description.tsx index faabbc1a143c5..f6462dc4b731b 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/description.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_description.tsx @@ -16,8 +16,8 @@ import { i18n } from '@kbn/i18n'; import { useRightPanelContext } from '../context'; import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; import { - DESCRIPTION_DETAILS_TEST_ID, - DESCRIPTION_TITLE_TEST_ID, + ALERT_DESCRIPTION_DETAILS_TEST_ID, + ALERT_DESCRIPTION_TITLE_TEST_ID, RULE_SUMMARY_BUTTON_TEST_ID, } from './test_ids'; import { @@ -27,10 +27,9 @@ import { } from '../../preview'; /** - * Displays the description of a document. - * If the document is an alert we show the rule description. If the document is of another type, we show -. + * Displays the rule description of a signal document. */ -export const Description: FC = () => { +export const AlertDescription: FC = () => { const { dataFormattedForFieldBrowser, scopeId, eventId, indexName, isPreview } = useRightPanelContext(); const { isAlert, ruleDescription, ruleName, ruleId } = useBasicDataFromDetailsData( @@ -98,7 +97,7 @@ export const Description: FC = () => { return ( - + {isAlert ? ( { )} - +

{ ); }; -Description.displayName = 'Description'; +AlertDescription.displayName = 'AlertDescription'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_title.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.test.tsx similarity index 56% rename from x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_title.test.tsx rename to x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.test.tsx index 95af8ecb3351b..61dbc57a2080a 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_title.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.test.tsx @@ -11,12 +11,12 @@ import { RightPanelContext } from '../context'; import { RISK_SCORE_VALUE_TEST_ID, SEVERITY_VALUE_TEST_ID, - FLYOUT_HEADER_TITLE_TEST_ID, + FLYOUT_ALERT_HEADER_TITLE_TEST_ID, STATUS_BUTTON_TEST_ID, ASSIGNEES_HEADER_TEST_ID, ALERT_SUMMARY_PANEL_TEST_ID, } from './test_ids'; -import { HeaderTitle } from './header_title'; +import { AlertHeaderTitle } from './alert_header_title'; import moment from 'moment-timezone'; import { useDateFormat, useTimeZone } from '../../../../common/lib/kibana'; import { mockGetFieldsData } from '../../shared/mocks/mock_get_fields_data'; @@ -33,17 +33,18 @@ const mockContextValue = { dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser, getFieldsData: jest.fn().mockImplementation(mockGetFieldsData), } as unknown as RightPanelContext; +const HEADER_TEXT_TEST_ID = `${FLYOUT_ALERT_HEADER_TITLE_TEST_ID}Text`; const renderHeader = (contextValue: RightPanelContext) => render( - + ); -describe('', () => { +describe('', () => { beforeEach(() => { jest.mocked(useDateFormat).mockImplementation(() => dateFormat); jest.mocked(useTimeZone).mockImplementation(() => 'UTC'); @@ -52,7 +53,7 @@ describe('', () => { it('should render component', () => { const { getByTestId } = renderHeader(mockContextValue); - expect(getByTestId(FLYOUT_HEADER_TITLE_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(HEADER_TEXT_TEST_ID)).toHaveTextContent('rule-name'); expect(getByTestId(SEVERITY_VALUE_TEST_ID)).toBeInTheDocument(); expect(getByTestId(ALERT_SUMMARY_PANEL_TEST_ID)).toBeInTheDocument(); @@ -61,49 +62,17 @@ describe('', () => { expect(getByTestId(ASSIGNEES_HEADER_TEST_ID)).toBeInTheDocument(); }); - it('should render rule name in the title if document is an alert', () => { - const { getByTestId } = renderHeader(mockContextValue); - - expect(getByTestId(FLYOUT_HEADER_TITLE_TEST_ID)).toHaveTextContent('rule-name'); - }); - - it('should render default document detail title if document is not an alert', () => { - const contextValue = { - ...mockContextValue, - dataFormattedForFieldBrowser: [ - { - category: 'kibana', - field: 'kibana.alert.rule.name', - values: [], - originalValue: [], - isObjectArray: false, - }, - ], - } as unknown as RightPanelContext; - - const { getByTestId } = renderHeader(contextValue); + it('should render title correctly if flyout is in preview', () => { + const { queryByTestId, getByTestId } = renderHeader({ ...mockContextValue, isPreview: true }); + expect(getByTestId(HEADER_TEXT_TEST_ID)).toHaveTextContent('rule-name'); - expect(getByTestId(FLYOUT_HEADER_TITLE_TEST_ID)).toHaveTextContent('Event details'); + expect(getByTestId(RISK_SCORE_VALUE_TEST_ID)).toBeInTheDocument(); + expect(queryByTestId(STATUS_BUTTON_TEST_ID)).not.toBeInTheDocument(); + expect(getByTestId(ASSIGNEES_HEADER_TEST_ID)).toHaveTextContent('Assignees—'); }); - it('should not render alert summary kpis if document is not an alert', () => { - const contextValue = { - ...mockContextValue, - dataFormattedForFieldBrowser: [ - { - category: 'kibana', - field: 'kibana.alert.rule.name', - values: [], - originalValue: [], - isObjectArray: false, - }, - ], - } as unknown as RightPanelContext; - - const { queryByTestId } = renderHeader(contextValue); - expect(queryByTestId(ALERT_SUMMARY_PANEL_TEST_ID)).not.toBeInTheDocument(); - expect(queryByTestId(STATUS_BUTTON_TEST_ID)).not.toBeInTheDocument(); - expect(queryByTestId(RISK_SCORE_VALUE_TEST_ID)).not.toBeInTheDocument(); - expect(queryByTestId(ASSIGNEES_HEADER_TEST_ID)).not.toBeInTheDocument(); + it('should render fall back values if document is not alert', () => { + const { getByTestId } = renderHeader({ ...mockContextValue, dataFormattedForFieldBrowser: [] }); + expect(getByTestId(HEADER_TEXT_TEST_ID)).toHaveTextContent('Document details'); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_title.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.tsx similarity index 59% rename from x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_title.tsx rename to x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.tsx index d4bbeb42d2e2f..8986eddbd17f4 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_title.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.tsx @@ -7,18 +7,10 @@ import type { FC } from 'react'; import React, { memo, useCallback, useMemo } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiPanel, - EuiTitle, - useEuiTheme, -} from '@elastic/eui'; -import { isEmpty } from 'lodash'; -import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiPanel, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/css'; import { ALERT_WORKFLOW_ASSIGNEE_IDS } from '@kbn/rule-data-utils'; +import { i18n } from '@kbn/i18n'; import { DocumentStatus } from './status'; import { DocumentSeverity } from './severity'; import { RiskScore } from './risk_score'; @@ -28,14 +20,14 @@ import { useRightPanelContext } from '../context'; import { PreferenceFormattedDate } from '../../../../common/components/formatted_date'; import { RenderRuleName } from '../../../../timelines/components/timeline/body/renderers/formatted_field_helpers'; import { SIGNAL_RULE_NAME_FIELD_NAME } from '../../../../timelines/components/timeline/body/renderers/constants'; -import { FLYOUT_HEADER_TITLE_TEST_ID, ALERT_SUMMARY_PANEL_TEST_ID } from './test_ids'; +import { FLYOUT_ALERT_HEADER_TITLE_TEST_ID, ALERT_SUMMARY_PANEL_TEST_ID } from './test_ids'; import { Assignees } from './assignees'; import { FlyoutTitle } from '../../../shared/components/flyout_title'; /** - * Document details flyout right section header + * Alert details flyout right section header */ -export const HeaderTitle: FC = memo(() => { +export const AlertHeaderTitle: FC = memo(() => { const { dataFormattedForFieldBrowser, eventId, @@ -55,7 +47,7 @@ export const HeaderTitle: FC = memo(() => { ) : ( { title={ruleName} iconType={'warning'} isLink - data-test-subj={FLYOUT_HEADER_TITLE_TEST_ID} + data-test-subj={FLYOUT_ALERT_HEADER_TITLE_TEST_ID} /> ), [ruleName, ruleId, eventId, scopeId, isPreview] ); - const eventTitle = ( - -

- -

- - ); - const { refetch } = useRefetchByScope({ scopeId }); const alertAssignees = useMemo( () => (getFieldsData(ALERT_WORKFLOW_ASSIGNEE_IDS) as string[]) ?? [], @@ -107,47 +88,53 @@ export const HeaderTitle: FC = memo(() => { {timestamp && } -
- {isAlert && !isEmpty(ruleName) ? ruleTitle : eventTitle} -
- - {isAlert && ( - - - - - - - - - - - - - + {isAlert && ruleName ? ( + ruleTitle + ) : ( + )} + + + + + + + + + + + + + + ); }); -HeaderTitle.displayName = 'HeaderTitle'; +AlertHeaderTitle.displayName = 'AlertHeaderTitle'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.test.tsx index 50b9a5b1643a4..7e5323e37b518 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.test.tsx @@ -9,6 +9,7 @@ import { render } from '@testing-library/react'; import React from 'react'; import { TestProviders } from '../../../../common/mock'; import { useAlertPrevalenceFromProcessTree } from '../../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; +import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; import { mockContextValue } from '../mocks/mock_context'; import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser'; import { RightPanelContext } from '../context'; @@ -21,6 +22,18 @@ jest.mock('../../../../common/containers/alerts/use_alert_prevalence_from_proces })); const mockUseAlertPrevalenceFromProcessTree = useAlertPrevalenceFromProcessTree as jest.Mock; +jest.mock('../../../../timelines/containers/use_timeline_data_filters', () => ({ + useTimelineDataFilters: jest.fn(), +})); +const mockUseTimelineDataFilters = useTimelineDataFilters as jest.Mock; + +const mockTreeValues = { + loading: false, + error: false, + alertIds: ['alertid'], + statsNodes: mock.mockStatsNodes, +}; + const renderAnalyzerPreview = (contextValue: RightPanelContext) => render( @@ -35,15 +48,11 @@ const NO_DATA_MESSAGE = 'An error is preventing this alert from being analyzed.' describe('', () => { beforeEach(() => { jest.clearAllMocks(); + mockUseTimelineDataFilters.mockReturnValue({ selectedPatterns: ['index'] }); }); it('shows analyzer preview correctly when documentId and index are present', () => { - mockUseAlertPrevalenceFromProcessTree.mockReturnValue({ - loading: false, - error: false, - alertIds: ['alertid'], - statsNodes: mock.mockStatsNodes, - }); + mockUseAlertPrevalenceFromProcessTree.mockReturnValue(mockTreeValues); const contextValue = { ...mockContextValue, dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser, @@ -59,13 +68,23 @@ describe('', () => { expect(wrapper.getByTestId(ANALYZER_PREVIEW_TEST_ID)).toBeInTheDocument(); }); - it('should use ancestor id when in preview', () => { - mockUseAlertPrevalenceFromProcessTree.mockReturnValue({ - loading: false, - error: false, - alertIds: ['alertid'], - statsNodes: mock.mockStatsNodes, + it('should use selected index patterns for non-alerts', () => { + mockUseAlertPrevalenceFromProcessTree.mockReturnValue(mockTreeValues); + const wrapper = renderAnalyzerPreview({ + ...mockContextValue, + dataFormattedForFieldBrowser: [], }); + + expect(mockUseAlertPrevalenceFromProcessTree).toHaveBeenCalledWith({ + isActiveTimeline: false, + documentId: 'eventId', + indices: ['index'], + }); + expect(wrapper.getByTestId(ANALYZER_PREVIEW_TEST_ID)).toBeInTheDocument(); + }); + + it('should use ancestor id as document id when in preview', () => { + mockUseAlertPrevalenceFromProcessTree.mockReturnValue(mockTreeValues); const contextValue = { ...mockContextValue, getFieldsData: () => 'ancestors-id', @@ -83,36 +102,6 @@ describe('', () => { expect(wrapper.getByTestId(ANALYZER_PREVIEW_TEST_ID)).toBeInTheDocument(); }); - it('shows error message when index is not present', () => { - mockUseAlertPrevalenceFromProcessTree.mockReturnValue({ - loading: false, - error: false, - alertIds: undefined, - statsNodes: undefined, - }); - const contextValue = { - ...mockContextValue, - dataFormattedForFieldBrowser: [ - { - category: 'kibana', - field: 'kibana.alert.rule.uuid', - values: ['rule-uuid'], - originalValue: ['rule-uuid'], - isObjectArray: false, - }, - ], - }; - const { getByText } = renderAnalyzerPreview(contextValue); - - expect(mockUseAlertPrevalenceFromProcessTree).toHaveBeenCalledWith({ - isActiveTimeline: false, - documentId: 'eventId', - indices: [], - }); - - expect(getByText(NO_DATA_MESSAGE)).toBeInTheDocument(); - }); - it('shows error message when there is an error', () => { mockUseAlertPrevalenceFromProcessTree.mockReturnValue({ loading: false, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.tsx index 5eeac1d9315a8..e87bc1fddbe92 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.tsx @@ -17,6 +17,7 @@ import { useAlertPrevalenceFromProcessTree } from '../../../../common/containers import type { StatsNode } from '../../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; import { isActiveTimeline } from '../../../../helpers'; import { getField } from '../../shared/utils'; +import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; const CHILD_COUNT_LIMIT = 3; const ANCESTOR_LEVEL = 3; @@ -42,11 +43,11 @@ export const AnalyzerPreview: React.FC = () => { isPreview, } = useRightPanelContext(); const ancestorId = getField(getFieldsData(ANCESTOR_ID)) ?? ''; - const documentId = isPreview ? ancestorId : eventId; // use ancestor as fallback for alert preview + const { selectedPatterns } = useTimelineDataFilters(isActiveTimeline(scopeId)); const index = find({ category: 'kibana', field: RULE_INDICES }, data); - const indices = index?.values ?? []; + const indices = index?.values ?? selectedPatterns; // adding sourcerer indices for non-alert documents const { statsNodes, loading, error } = useAlertPrevalenceFromProcessTree({ isActiveTimeline: isActiveTimeline(scopeId), @@ -65,7 +66,7 @@ export const AnalyzerPreview: React.FC = () => { [cache.statsNodes] ); - const showAnalyzerTree = eventId && index && items && items.length > 0 && !error; + const showAnalyzerTree = items && items.length > 0 && !error; return loading ? ( + render( + + + + + + ); + +describe('', () => { + it('should render description for 1 category', () => { + const mockGetFieldsData = (field: string) => { + switch (field) { + case 'event.kind': + return 'event'; + case 'event.category': + return 'file'; + } + }; + const { getByTestId } = renderDescription({ + ...mockContextValue, + getFieldsData: mockGetFieldsData, + }); + + expect(getByTestId(`${EVENT_CATEGORY_DESCRIPTION_TEST_ID}-file`)).toBeInTheDocument(); + }); + + it('should render description for multiple categories', () => { + const mockGetFieldsData = (field: string) => { + switch (field) { + case 'event.kind': + return 'event'; + case 'event.category': + return ['file', 'network']; + } + }; + const { getByTestId } = renderDescription({ + ...mockContextValue, + getFieldsData: mockGetFieldsData, + }); + + expect(getByTestId(`${EVENT_CATEGORY_DESCRIPTION_TEST_ID}-file`)).toBeInTheDocument(); + expect(getByTestId(`${EVENT_CATEGORY_DESCRIPTION_TEST_ID}-network`)).toBeInTheDocument(); + }); + + it('should render category name and fallback description if not ecs compliant', () => { + const mockGetFieldsData = (field: string) => { + switch (field) { + case 'event.kind': + return 'event'; + case 'event.category': + return 'behavior'; + } + }; + const { getByTestId } = renderDescription({ + ...mockContextValue, + getFieldsData: mockGetFieldsData, + }); + + expect(getByTestId(`${EVENT_CATEGORY_DESCRIPTION_TEST_ID}-behavior`)).toHaveTextContent( + 'BehaviorThis field is not an ecs field, description is not available.' + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_category_description.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_category_description.tsx new file mode 100644 index 0000000000000..2a9048a0504e5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_category_description.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, { useMemo } from 'react'; +import { EuiFlexItem, EuiTitle, EuiSpacer, EuiText } from '@elastic/eui'; +import { startCase } from 'lodash'; +import { useRightPanelContext } from '../context'; +import { getEcsAllowedValueDescription } from '../utils/event_utils'; +import { getFieldArray } from '../../shared/utils'; +import { EVENT_CATEGORY_DESCRIPTION_TEST_ID } from './test_ids'; + +/** + * Displays the category description of an event document. + */ +export const EventCategoryDescription: React.FC = () => { + const { getFieldsData } = useRightPanelContext(); + const eventCategories = useMemo( + () => getFieldArray(getFieldsData('event.category')), + [getFieldsData] + ); + + return ( + <> + {eventCategories.map((category) => ( + + +
{startCase(category)}
+
+ + {getEcsAllowedValueDescription('event.category', category)} +
+ ))} + + ); +}; + +EventCategoryDescription.displayName = 'EventCategoryDescription'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_header_title.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_header_title.test.tsx new file mode 100644 index 0000000000000..11f5719c7b930 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_header_title.test.tsx @@ -0,0 +1,90 @@ +/* + * 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 { render } from '@testing-library/react'; +import { RightPanelContext } from '../context'; +import { SEVERITY_VALUE_TEST_ID, FLYOUT_EVENT_HEADER_TITLE_TEST_ID } from './test_ids'; +import { EventHeaderTitle } from './event_header_title'; +import moment from 'moment-timezone'; +import { useDateFormat, useTimeZone } from '../../../../common/lib/kibana'; +import { mockContextValue } from '../mocks/mock_context'; +import { TestProvidersComponent } from '../../../../common/mock'; + +jest.mock('../../../../common/lib/kibana'); + +moment.suppressDeprecationWarnings = true; +moment.tz.setDefault('UTC'); + +const dateFormat = 'MMM D, YYYY @ HH:mm:ss.SSS'; + +const renderHeader = (contextValue: RightPanelContext) => + render( + + + + + + ); + +const EVENT_HEADER_TEXT_TEST_ID = `${FLYOUT_EVENT_HEADER_TITLE_TEST_ID}Text`; + +describe('', () => { + beforeEach(() => { + jest.mocked(useDateFormat).mockImplementation(() => dateFormat); + jest.mocked(useTimeZone).mockImplementation(() => 'UTC'); + }); + + it('should render component', () => { + const { getByTestId } = renderHeader(mockContextValue); + + expect(getByTestId(EVENT_HEADER_TEXT_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(SEVERITY_VALUE_TEST_ID)).toBeInTheDocument(); + }); + + it('should render event details as title if event.kind is not event', () => { + const mockGetFieldsData = (field: string) => { + switch (field) { + case 'event.kind': + return 'alert'; + case 'event.category': + return 'malware'; + } + }; + const { getByTestId } = renderHeader({ + ...mockContextValue, + getFieldsData: mockGetFieldsData, + }); + + expect(getByTestId(EVENT_HEADER_TEXT_TEST_ID)).toHaveTextContent('Event details'); + }); + + it('should render event category as title if event.kind is event', () => { + const mockGetFieldsData = (field: string) => { + switch (field) { + case 'event.kind': + return 'event'; + case 'event.category': + return 'process'; + case 'process.name': + return 'process name'; + } + }; + const { getByTestId } = renderHeader({ + ...mockContextValue, + getFieldsData: mockGetFieldsData, + }); + + expect(getByTestId(EVENT_HEADER_TEXT_TEST_ID)).toHaveTextContent('process name'); + }); + + it('should fallback title if event kind is null', () => { + const { getByTestId } = renderHeader({ ...mockContextValue, getFieldsData: () => '' }); + + expect(getByTestId(EVENT_HEADER_TEXT_TEST_ID)).toHaveTextContent('Event details'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_header_title.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_header_title.tsx new file mode 100644 index 0000000000000..76a9c17ff886e --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_header_title.tsx @@ -0,0 +1,60 @@ +/* + * 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 type { FC } from 'react'; +import React, { memo, useMemo } from 'react'; +import { EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FlyoutTitle } from '../../../shared/components/flyout_title'; +import { DocumentSeverity } from './severity'; +import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; +import { useRightPanelContext } from '../context'; +import { PreferenceFormattedDate } from '../../../../common/components/formatted_date'; +import { FLYOUT_EVENT_HEADER_TITLE_TEST_ID } from './test_ids'; +import { getField } from '../../shared/utils'; +import { EVENT_CATEGORY_TO_FIELD } from '../utils/event_utils'; + +/** + * Event details flyout right section header + */ +export const EventHeaderTitle: FC = memo(() => { + const { dataFormattedForFieldBrowser, getFieldsData } = useRightPanelContext(); + const { timestamp } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser); + + const eventKind = getField(getFieldsData('event.kind')); + const eventCategory = getField(getFieldsData('event.category')); + + const title = useMemo(() => { + let eventTitle; + if (eventKind === 'event' && eventCategory) { + const fieldName = EVENT_CATEGORY_TO_FIELD[eventCategory]; + eventTitle = getField(getFieldsData(fieldName)); + } + return ( + eventTitle ?? + i18n.translate('xpack.securitySolution.flyout.right.title.eventTitle', { + defaultMessage: 'Event details', + }) + ); + }, [eventKind, getFieldsData, eventCategory]); + + return ( + <> + + + {timestamp && } + + + + ); +}); + +EventHeaderTitle.displayName = 'EventHeaderTitle'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_kind_description.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_kind_description.test.tsx new file mode 100644 index 0000000000000..6c7f69b175f03 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_kind_description.test.tsx @@ -0,0 +1,77 @@ +/* + * 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 { render } from '@testing-library/react'; +import { RightPanelContext } from '../context'; +import { + EVENT_KIND_DESCRIPTION_TEST_ID, + EVENT_KIND_DESCRIPTION_TEXT_TEST_ID, + EVENT_KIND_DESCRIPTION_CATEGORIES_TEST_ID, +} from './test_ids'; +import { EventKindDescription } from './event_kind_description'; +import { mockContextValue } from '../mocks/mock_context'; +import { TestProvidersComponent } from '../../../../common/mock'; + +const renderDescription = (contextValue: RightPanelContext) => + render( + + + + + + ); + +describe('', () => { + describe('event kind description section', () => { + it('should render event kind title', () => { + const { getByTestId } = renderDescription(mockContextValue); + expect(getByTestId(EVENT_KIND_DESCRIPTION_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(EVENT_KIND_DESCRIPTION_TEXT_TEST_ID)).toHaveTextContent('Alert'); + }); + }); + + describe('event categories section', () => { + it('should render event category correctly for 1 category', () => { + const mockGetFieldsData = (field: string) => { + switch (field) { + case 'event.category': + return 'behavior'; + } + }; + const { getByTestId } = renderDescription({ + ...mockContextValue, + getFieldsData: mockGetFieldsData, + }); + + expect(getByTestId(EVENT_KIND_DESCRIPTION_CATEGORIES_TEST_ID)).toHaveTextContent('behavior'); + }); + + it('should render event category for multiple categories', () => { + const mockGetFieldsData = (field: string) => { + switch (field) { + case 'event.category': + return ['session', 'authentication']; + } + }; + const { getByTestId } = renderDescription({ + ...mockContextValue, + getFieldsData: mockGetFieldsData, + }); + + expect(getByTestId(EVENT_KIND_DESCRIPTION_CATEGORIES_TEST_ID)).toHaveTextContent( + 'session,authentication' + ); + }); + + it('should not render category name if not available', () => { + const { queryByTestId } = renderDescription(mockContextValue); + + expect(queryByTestId(EVENT_KIND_DESCRIPTION_CATEGORIES_TEST_ID)).not.toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_kind_description.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_kind_description.tsx new file mode 100644 index 0000000000000..21e4a6a2d4a49 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_kind_description.tsx @@ -0,0 +1,80 @@ +/* + * 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, { useMemo } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer, EuiText, EuiToolTip } from '@elastic/eui'; +import { startCase } from 'lodash'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useRightPanelContext } from '../context'; +import { getEcsAllowedValueDescription } from '../utils/event_utils'; +import { getFieldArray } from '../../shared/utils'; +import { + EVENT_KIND_DESCRIPTION_TEST_ID, + EVENT_KIND_DESCRIPTION_TEXT_TEST_ID, + EVENT_KIND_DESCRIPTION_CATEGORIES_TEST_ID, +} from './test_ids'; + +export interface EventKindDescriptionProps { + /** + * Event kind field from ecs + */ + eventKind: string; +} + +/** + * Display description of a document at the event kind level + * Shows the ecs description of the event kind, and a list of event categories + */ +export const EventKindDescription: React.FC = ({ eventKind }) => { + const { getFieldsData } = useRightPanelContext(); + const eventCategories = useMemo( + () => getFieldArray(getFieldsData('event.category')), + [getFieldsData] + ); + + return ( + + +
{startCase(eventKind)}
+
+ + {getEcsAllowedValueDescription('event.kind', eventKind)} + {eventCategories.length > 0 && ( + <> + + +
+ +
+
+ + + {eventCategories.map((category, idx) => ( + + + + {category} + {idx !== eventCategories.length - 1 && ','} + + + + ))} + + + )} +
+ ); +}; + +EventKindDescription.displayName = 'EventKindDescription'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_renderer.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_renderer.test.tsx new file mode 100644 index 0000000000000..578147cc08999 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_renderer.test.tsx @@ -0,0 +1,51 @@ +/* + * 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 { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; +import { render } from '@testing-library/react'; +import { ThemeProvider } from 'styled-components'; +import { EventRenderer } from './event_renderer'; +import { RightPanelContext } from '../context'; +import { EVENT_RENDERER_TEST_ID } from './test_ids'; +import { mockContextValue } from '../mocks/mock_context'; +import { mockDataAsNestedObject } from '../../shared/mocks/mock_data_as_nested_object'; +import { getMockTheme } from '../../../../common/lib/kibana/kibana_react.mock'; + +const mockTheme = getMockTheme({ eui: { euiFontSizeXS: '' } }); +const renderEventRenderer = (contextValue: RightPanelContext) => + render( + + + + + + + + ); + +describe('', () => { + it('should render conponent', async () => { + const contextValue = { + ...mockContextValue, + dataAsNestedObject: { + ...mockDataAsNestedObject, + // using suricate because it's the simpliest to set up, for more details on how the event renderes are evaluated + // see security_solutions/public/timelines/components/timeline/body/renderers/get_row_renderer.ts + event: { ...mockDataAsNestedObject.event, module: ['suricata'] }, + }, + }; + const { getByTestId } = renderEventRenderer(contextValue); + expect(getByTestId(EVENT_RENDERER_TEST_ID)).toBeInTheDocument(); + }); + + it('should render empty component if event renderer is not available', async () => { + const { container } = renderEventRenderer({} as unknown as RightPanelContext); + + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_renderer.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_renderer.tsx new file mode 100644 index 0000000000000..54e60d0331b81 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_renderer.tsx @@ -0,0 +1,65 @@ +/* + * 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 type { FC } from 'react'; +import React, { useMemo } from 'react'; +import { EuiFlexItem, EuiTitle } from '@elastic/eui'; +import styled from '@emotion/styled'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { getRowRenderer } from '../../../../timelines/components/timeline/body/renderers/get_row_renderer'; +import { defaultRowRenderers } from '../../../../timelines/components/timeline/body/renderers'; +import { useRightPanelContext } from '../context'; +import { EVENT_RENDERER_TEST_ID } from './test_ids'; + +const ReasonPreviewContainerWrapper = styled.div` + overflow-x: auto; + padding-block: ${euiThemeVars.euiSizeS}; +`; + +const ReasonPreviewContainer = styled.div``; + +/** + * Event renderer of an event document + */ +export const EventRenderer: FC = () => { + const { dataAsNestedObject, scopeId } = useRightPanelContext(); + + const renderer = useMemo( + () => getRowRenderer({ data: dataAsNestedObject, rowRenderers: defaultRowRenderers }), + [dataAsNestedObject] + ); + const rowRenderer = useMemo( + () => + renderer + ? renderer.renderRow({ + contextId: 'event-details', + data: dataAsNestedObject, + isDraggable: false, + scopeId, + }) + : null, + [renderer, dataAsNestedObject, scopeId] + ); + + if (!renderer) { + return null; + } + return ( + + +
{'Event renderer'}
+
+ + + {rowRenderer} + + +
+ ); +}; + +EventRenderer.displayName = 'EventRenderer'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/expandable_section.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/expandable_section.tsx index d3c95661f7fe5..e7e2c846367c5 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/expandable_section.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/expandable_section.tsx @@ -6,6 +6,7 @@ */ import { EuiAccordion, EuiFlexGroup, EuiSpacer, EuiTitle, useGeneratedHtmlId } from '@elastic/eui'; +import type { EuiFlexGroupProps } from '@elastic/eui'; import type { ReactElement } from 'react'; import React, { type VFC } from 'react'; import { useAccordionState } from '../hooks/use_accordion_state'; @@ -22,6 +23,10 @@ export interface DescriptionSectionProps { * Title value to render in the header of the accordion */ title: ReactElement; + /** + * Gutter size between contents in expandable section + */ + gutterSize?: EuiFlexGroupProps['gutterSize']; /** * React component to render in the expandable section of the accordion */ @@ -43,6 +48,7 @@ export const ExpandableSection: VFC = ({ expanded, title, children, + gutterSize = 'none', 'data-test-subj': dataTestSub, }) => { const accordionId = useGeneratedHtmlId({ prefix: 'accordion' }); @@ -61,7 +67,7 @@ export const ExpandableSection: VFC = ({ return ( - + {renderContent && children} diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_section.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_section.test.tsx index bef0118295470..25aa9a5ddc699 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_section.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_section.test.tsx @@ -8,7 +8,11 @@ import React from 'react'; import { render } from '@testing-library/react'; import { RightPanelContext } from '../context'; -import { INSIGHTS_HEADER_TEST_ID } from './test_ids'; +import { + INSIGHTS_HEADER_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_TEST_ID, + CORRELATIONS_TEST_ID, +} from './test_ids'; import { TestProviders } from '../../../../common/mock'; import { useFirstLastSeen } from '../../../../common/containers/use_first_last_seen'; import { useObservedUserDetails } from '../../../../explore/users/containers/users/observed_details'; @@ -134,4 +138,24 @@ describe('', () => { expect(wrapper.getAllByRole('button')[0]).toHaveAttribute('aria-expanded', 'true'); expect(wrapper.getAllByRole('button')[0]).not.toHaveAttribute('disabled'); }); + + it('should not render threat intel and correlations insights component when document is not signal', () => { + const getFieldsData = (field: string) => { + switch (field) { + case 'event.kind': + return 'metric'; + } + }; + const contextValue = { + eventId: 'some_Id', + getFieldsData, + documentIsSignal: false, + } as unknown as RightPanelContext; + + const { getByTestId, queryByTestId } = renderInsightsSection(contextValue, false); + + expect(getByTestId(INSIGHTS_HEADER_TEST_ID)).toBeInTheDocument(); + expect(queryByTestId(INSIGHTS_THREAT_INTELLIGENCE_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(CORRELATIONS_TEST_ID)).not.toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_section.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_section.tsx index a51d5c8fec3fb..4c1c12f5e1bd7 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_section.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_section.tsx @@ -14,6 +14,9 @@ import { ThreatIntelligenceOverview } from './threat_intelligence_overview'; import { INSIGHTS_TEST_ID } from './test_ids'; import { EntitiesOverview } from './entities_overview'; import { ExpandableSection } from './expandable_section'; +import { useRightPanelContext } from '../context'; +import { getField } from '../../shared/utils'; +import { EventKind } from '../../shared/constants/event_kinds'; export interface InsightsSectionProps { /** @@ -26,6 +29,9 @@ export interface InsightsSectionProps { * Insights section under overview tab. It contains entities, threat intelligence, prevalence and correlations. */ export const InsightsSection: React.FC = ({ expanded = false }) => { + const { getFieldsData } = useRightPanelContext(); + const eventKind = getField(getFieldsData('event.kind')); + return ( = ({ expanded = fal data-test-subj={INSIGHTS_TEST_ID} > - - - - + {eventKind === EventKind.signal && ( + <> + + + + + + )} diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/investigation_section.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/investigation_section.test.tsx index be223f1afd986..e240b1d5636be 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/investigation_section.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/investigation_section.test.tsx @@ -11,26 +11,30 @@ import { render } from '@testing-library/react'; import { INVESTIGATION_SECTION_CONTENT_TEST_ID, INVESTIGATION_SECTION_HEADER_TEST_ID, + INVESTIGATION_GUIDE_TEST_ID, + HIGHLIGHTED_FIELDS_TITLE_TEST_ID, } from './test_ids'; import { RightPanelContext } from '../context'; import { InvestigationSection } from './investigation_section'; import { useRuleWithFallback } from '../../../../detection_engine/rule_management/logic/use_rule_with_fallback'; import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser'; import { TestProvider } from '@kbn/expandable-flyout/src/test/provider'; +import { mockContextValue } from '../mocks/mock_context'; jest.mock('../../../../detection_engine/rule_management/logic/use_rule_with_fallback'); const panelContextValue = { + ...mockContextValue, dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser.filter( (d) => d.field !== 'kibana.alert.rule.type' ), -} as unknown as RightPanelContext; +}; -const renderInvestigationSection = (expanded: boolean = false) => +const renderInvestigationSection = (expanded: boolean = false, contextValue = panelContextValue) => render( - + @@ -42,23 +46,42 @@ describe('', () => { jest.clearAllMocks(); (useRuleWithFallback as jest.Mock).mockReturnValue({ rule: { note: 'test note' } }); }); + it('should render the component collapsed', () => { const { getByTestId } = renderInvestigationSection(); - expect(getByTestId(INVESTIGATION_SECTION_HEADER_TEST_ID)).toBeInTheDocument(); }); it('should render the component expanded', () => { const { getByTestId } = renderInvestigationSection(true); - expect(getByTestId(INVESTIGATION_SECTION_HEADER_TEST_ID)).toBeInTheDocument(); expect(getByTestId(INVESTIGATION_SECTION_CONTENT_TEST_ID)).toBeInTheDocument(); }); it('should expand the component when clicking on the arrow on header', () => { const { getByTestId } = renderInvestigationSection(); - getByTestId(INVESTIGATION_SECTION_HEADER_TEST_ID).click(); expect(getByTestId(INVESTIGATION_SECTION_CONTENT_TEST_ID)).toBeInTheDocument(); }); + + it('should render investigation guide and highlighted fields when document is signal', () => { + const { getByTestId } = renderInvestigationSection(true); + expect(getByTestId(INVESTIGATION_GUIDE_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(HIGHLIGHTED_FIELDS_TITLE_TEST_ID)).toBeInTheDocument(); + }); + + it('should not render investigation guide when document is not signal', () => { + const mockGetFieldsData = (field: string) => { + switch (field) { + case 'event.kind': + return 'alert'; + } + }; + const { getByTestId, queryByTestId } = renderInvestigationSection(true, { + ...panelContextValue, + getFieldsData: mockGetFieldsData, + }); + expect(queryByTestId(INVESTIGATION_GUIDE_TEST_ID)).not.toBeInTheDocument(); + expect(getByTestId(HIGHLIGHTED_FIELDS_TITLE_TEST_ID)).toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/investigation_section.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/investigation_section.tsx index 2cf91392ff154..875858bbca2f7 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/investigation_section.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/investigation_section.tsx @@ -13,6 +13,10 @@ import { ExpandableSection } from './expandable_section'; import { HighlightedFields } from './highlighted_fields'; import { INVESTIGATION_SECTION_TEST_ID } from './test_ids'; import { InvestigationGuide } from './investigation_guide'; +import { getField } from '../../shared/utils'; +import { EventKind } from '../../shared/constants/event_kinds'; +import { useRightPanelContext } from '../context'; + export interface DescriptionSectionProps { /** * Boolean to allow the component to be expanded or collapsed on first render @@ -21,9 +25,13 @@ export interface DescriptionSectionProps { } /** - * Most top section of the overview tab. It contains the description, reason and mitre attack information (for a document of type alert). + * Second section of the overview tab in details flyout. + * It contains investigation guide (alerts only) and highlighted fields */ export const InvestigationSection: VFC = ({ expanded = true }) => { + const { getFieldsData } = useRightPanelContext(); + const eventKind = getField(getFieldsData('event.kind')); + return ( = ({ expanded = /> } data-test-subj={INVESTIGATION_SECTION_TEST_ID} + gutterSize="s" > - - + {eventKind === EventKind.signal && ( + <> + + + + )} ); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/response_section.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/response_section.test.tsx index 9cd09e0edbda7..dc421aac72e0f 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/response_section.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/response_section.test.tsx @@ -10,18 +10,17 @@ import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { render } from '@testing-library/react'; import { RESPONSE_SECTION_CONTENT_TEST_ID, RESPONSE_SECTION_HEADER_TEST_ID } from './test_ids'; import { RightPanelContext } from '../context'; +import { mockContextValue } from '../mocks/mock_context'; import { ResponseSection } from './response_section'; import { TestProvider } from '@kbn/expandable-flyout/src/test/provider'; const PREVIEW_MESSAGE = 'Response is not available in alert preview.'; -const panelContextValue = {} as unknown as RightPanelContext; - const renderResponseSection = () => render( - + @@ -53,7 +52,7 @@ describe('', () => { const { getByTestId } = render( - + @@ -62,4 +61,28 @@ describe('', () => { getByTestId(RESPONSE_SECTION_HEADER_TEST_ID).click(); expect(getByTestId(RESPONSE_SECTION_CONTENT_TEST_ID)).toHaveTextContent(PREVIEW_MESSAGE); }); + + it('should render empty component if document is not signal', () => { + const mockGetFieldsData = (field: string) => { + switch (field) { + case 'event.kind': + return 'event'; + } + }; + const { container } = render( + + + + + + + + ); + expect(container).toBeEmptyDOMElement(); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/response_section.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/response_section.tsx index 6b7ca6e282246..0f5951d2a4952 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/response_section.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/response_section.tsx @@ -11,7 +11,10 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { ResponseButton } from './response_button'; import { ExpandableSection } from './expandable_section'; import { useRightPanelContext } from '../context'; +import { getField } from '../../shared/utils'; +import { EventKind } from '../../shared/constants/event_kinds'; import { RESPONSE_SECTION_TEST_ID } from './test_ids'; + export interface ResponseSectionProps { /** * Boolean to allow the component to be expanded or collapsed on first render @@ -23,7 +26,12 @@ export interface ResponseSectionProps { * Most bottom section of the overview tab. It contains a summary of the response tab. */ export const ResponseSection: VFC = ({ expanded = false }) => { - const { isPreview } = useRightPanelContext(); + const { isPreview, getFieldsData } = useRightPanelContext(); + const eventKind = getField(getFieldsData('event.kind')); + if (eventKind !== EventKind.signal) { + return null; + } + return ( ({ @@ -22,6 +23,11 @@ jest.mock('../../../../common/containers/alerts/use_alert_prevalence_from_proces })); const mockUseAlertPrevalenceFromProcessTree = useAlertPrevalenceFromProcessTree as jest.Mock; +jest.mock('../../../../timelines/containers/use_timeline_data_filters', () => ({ + useTimelineDataFilters: jest.fn(), +})); +const mockUseTimelineDataFilters = useTimelineDataFilters as jest.Mock; + const contextValue = { ...mockContextValue, dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser, @@ -29,6 +35,7 @@ const contextValue = { describe('', () => { beforeEach(() => { + mockUseTimelineDataFilters.mockReturnValue({ selectedPatterns: ['index'] }); mockUseAlertPrevalenceFromProcessTree.mockReturnValue({ loading: false, error: false, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/content.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/content.tsx index db5b13cf123e2..55240ab65265d 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/content.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/content.tsx @@ -9,7 +9,7 @@ import type { VFC } from 'react'; import React, { useMemo } from 'react'; import { FLYOUT_BODY_TEST_ID } from './test_ids'; import type { RightPanelPaths } from '.'; -import type { RightPanelTabsType } from './tabs'; +import type { RightPanelTabType } from './tabs'; import { FlyoutBody } from '../../shared/components/flyout_body'; export interface PanelContentProps { @@ -20,7 +20,7 @@ export interface PanelContentProps { /** * Tabs display right below the flyout's header */ - tabs: RightPanelTabsType; + tabs: RightPanelTabType[]; } /** diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/header.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/header.tsx index 569b7f6baab2f..70c27e18faa23 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/header.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/header.tsx @@ -9,10 +9,13 @@ import { EuiSpacer, EuiTab } from '@elastic/eui'; import type { FC } from 'react'; import React, { memo } from 'react'; import type { RightPanelPaths } from '.'; -import type { RightPanelTabsType } from './tabs'; +import type { RightPanelTabType } from './tabs'; import { FlyoutHeader } from '../../shared/components/flyout_header'; import { FlyoutHeaderTabs } from '../../shared/components/flyout_header_tabs'; -import { HeaderTitle } from './components/header_title'; +import { AlertHeaderTitle } from './components/alert_header_title'; +import { EventHeaderTitle } from './components/event_header_title'; +import { useRightPanelContext } from './context'; +import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers'; export interface PanelHeaderProps { /** @@ -27,11 +30,13 @@ export interface PanelHeaderProps { /** * Tabs to display in the header */ - tabs: RightPanelTabsType; + tabs: RightPanelTabType[]; } export const PanelHeader: FC = memo( ({ selectedTabId, setSelectedTabId, tabs }) => { + const { dataFormattedForFieldBrowser } = useRightPanelContext(); + const { isAlert } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser); const onSelectedTabChanged = (id: RightPanelPaths) => setSelectedTabId(id); const renderTabs = tabs.map((tab, index) => ( = memo( return ( - + {isAlert ? : } {renderTabs} diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_flyout_is_expandable.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_flyout_is_expandable.test.tsx new file mode 100644 index 0000000000000..a67bb675a373a --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_flyout_is_expandable.test.tsx @@ -0,0 +1,143 @@ +/* + * 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 { useFlyoutIsExpandable } from './use_flyout_is_expandable'; +import { renderHook } from '@testing-library/react-hooks'; +import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; + +const getFieldsData = jest.fn(); +jest.mock('../../../../common/hooks/use_experimental_features', () => ({ + useIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(true), +})); +const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; + +describe('useFlyoutIsExpandable', () => { + it('always return ture when event.kind is signal (alert document)', () => { + const dataAsNestedObject = {} as unknown as Ecs; + getFieldsData.mockImplementation((field: string) => { + if (field === 'event.kind') { + return 'signal'; + } + }); + const hookResult = renderHook(() => + useFlyoutIsExpandable({ getFieldsData, dataAsNestedObject }).valueOf() + ); + expect(hookResult.result.current).toBe(true); + }); + + it('always return false when event.kind is not signal and feature flag is off', () => { + const dataAsNestedObject = {} as unknown as Ecs; + useIsExperimentalFeatureEnabledMock.mockReturnValue(false); + getFieldsData.mockImplementation((field: string) => { + if (field === 'event.kind') { + return 'signal'; + } + }); + const hookResult = renderHook(() => + useFlyoutIsExpandable({ getFieldsData, dataAsNestedObject }).valueOf() + ); + expect(hookResult.result.current).toBe(true); + }); + + describe('event renderer is not available', () => { + beforeEach(() => { + useIsExperimentalFeatureEnabledMock.mockReturnValue(true); + }); + const dataAsNestedObject = {} as unknown as Ecs; + describe('event.kind is not event', () => { + it('should return true if event.kind is in ecs allowed values', () => { + getFieldsData.mockImplementation((field: string) => { + if (field === 'event.kind') { + return 'alert'; + } + }); + const hookResult = renderHook(() => + useFlyoutIsExpandable({ getFieldsData, dataAsNestedObject }).valueOf() + ); + expect(hookResult.result.current).toBe(true); + }); + + it('should return false if event.kind is not an ecs allowed values', () => { + getFieldsData.mockImplementation((field: string) => { + if (field === 'event.kind') { + return 'not ecs'; + } + }); + const hookResult = renderHook(() => + useFlyoutIsExpandable({ getFieldsData, dataAsNestedObject }) + ); + expect(hookResult.result.current).toBe(false); + }); + + it('should return false if event.kind is notavailable', () => { + getFieldsData.mockImplementation(() => {}); + const hookResult = renderHook(() => + useFlyoutIsExpandable({ getFieldsData, dataAsNestedObject }) + ); + expect(hookResult.result.current).toBe(false); + }); + }); + + describe('event.kind is event', () => { + it('should return true if event.category is in ecs allowed values', () => { + getFieldsData.mockImplementation((field: string) => { + if (field === 'event.kind') { + return 'event'; + } + if (field === 'event.category') { + return 'file'; + } + }); + const hookResult = renderHook(() => + useFlyoutIsExpandable({ getFieldsData, dataAsNestedObject }) + ); + expect(hookResult.result.current).toBe(true); + }); + + it('should return false if event.category is not an ecs allowed values', () => { + getFieldsData.mockImplementation((field: string) => { + if (field === 'event.kind') { + return 'event'; + } + if (field === 'event.category') { + return 'not ecs'; + } + }); + const hookResult = renderHook(() => + useFlyoutIsExpandable({ getFieldsData, dataAsNestedObject }) + ); + expect(hookResult.result.current).toBe(false); + }); + it('should return false if event.category is not available', () => { + getFieldsData.mockImplementation((field: string) => { + if (field === 'event.kind') { + return 'event'; + } + }); + const hookResult = renderHook(() => + useFlyoutIsExpandable({ getFieldsData, dataAsNestedObject }) + ); + expect(hookResult.result.current).toBe(false); + }); + }); + }); + + describe('event renderer is available', () => { + const dataAsNestedObject = { event: { module: ['suricata'] } } as unknown as Ecs; + beforeEach(() => { + useIsExperimentalFeatureEnabledMock.mockReturnValue(true); + }); + + it('should return true', () => { + const hookResult = renderHook(() => + useFlyoutIsExpandable({ getFieldsData, dataAsNestedObject }) + ); + expect(hookResult.result.current).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_flyout_is_expandable.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_flyout_is_expandable.ts new file mode 100644 index 0000000000000..8e465b31ce8e0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_flyout_is_expandable.ts @@ -0,0 +1,68 @@ +/* + * 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 { useMemo } from 'react'; +import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; +import { getField, getFieldArray } from '../../shared/utils'; +import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data'; +import { getRowRenderer } from '../../../../timelines/components/timeline/body/renderers/get_row_renderer'; +import { defaultRowRenderers } from '../../../../timelines/components/timeline/body/renderers'; +import { isEcsAllowedValue } from '../utils/event_utils'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { EventKind } from '../../shared/constants/event_kinds'; + +export interface UseShowEventOverviewParams { + /** + * Retrieves searchHit values for the provided field + */ + getFieldsData: GetFieldsData; + /** + * An object with top level fields from the ECS object + */ + dataAsNestedObject: Ecs; +} + +/** + * Hook used on the right panel to decide if the flyout has an expanded section. + * This also helps deciding if the overview section should be displayed. + * The hook looks at the `event.kind` and `event.category` fields of the document. + */ +export const useFlyoutIsExpandable = ({ + getFieldsData, + dataAsNestedObject, +}: UseShowEventOverviewParams): boolean => { + const renderer = getRowRenderer({ data: dataAsNestedObject, rowRenderers: defaultRowRenderers }); + + const eventKind = getField(getFieldsData('event.kind')); + const eventKindInECS = eventKind && isEcsAllowedValue('event.kind', eventKind); + + const eventCategories = getFieldArray(getFieldsData('event.category')); + const eventCategoryInECS = eventCategories.some((category) => + isEcsAllowedValue('event.category', category) + ); + + const expandableEventFlyoutEnabled = useIsExperimentalFeatureEnabled( + 'expandableEventFlyoutEnabled' + ); + + return useMemo(() => { + // alert document: always show overview + if (eventKind === EventKind.signal) { + return true; + } + // do not show overview for non-alert if feature flag is disabled + if (!expandableEventFlyoutEnabled) { + return false; + } + // event document: show overview when event category is ecs compliant or event renderer is available + if (eventKind === EventKind.event) { + return eventCategoryInECS || renderer != null; + } + // non-event document: show overview when event kind is ecs compliant or event renderer is available + return eventKindInECS || renderer != null; + }, [expandableEventFlyoutEnabled, eventKind, eventCategoryInECS, eventKindInECS, renderer]); +}; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/index.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/index.tsx index 7501b55e407ca..52bb1aa45daf2 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/index.tsx @@ -9,15 +9,14 @@ import type { FC } from 'react'; import React, { memo, useMemo, useEffect } from 'react'; import type { FlyoutPanelProps, PanelPath } from '@kbn/expandable-flyout'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; -import { EventKind } from '../shared/constants/event_kinds'; -import { getField } from '../shared/utils'; import { useRightPanelContext } from './context'; import { PanelNavigation } from './navigation'; import { PanelHeader } from './header'; import { PanelContent } from './content'; -import type { RightPanelTabsType } from './tabs'; -import { tabs } from './tabs'; +import type { RightPanelTabType } from './tabs'; +import * as tabs from './tabs'; import { PanelFooter } from './footer'; +import { useFlyoutIsExpandable } from './hooks/use_flyout_is_expandable'; export type RightPanelPaths = 'overview' | 'table' | 'json'; export const DocumentDetailsRightPanelKey: RightPanelProps['key'] = 'document-details-right'; @@ -37,11 +36,15 @@ export interface RightPanelProps extends FlyoutPanelProps { */ export const RightPanel: FC> = memo(({ path }) => { const { openRightPanel, closeFlyout } = useExpandableFlyoutApi(); - const { eventId, getFieldsData, indexName, scopeId, isPreview } = useRightPanelContext(); + const { eventId, indexName, scopeId, isPreview, dataAsNestedObject, getFieldsData } = + useRightPanelContext(); - // for 8.10, we only render the flyout in its expandable mode if the document viewed is of type signal - const documentIsSignal = getField(getFieldsData('event.kind')) === EventKind.signal; - const tabsDisplayed = documentIsSignal ? tabs : tabs.filter((tab) => tab.id !== 'overview'); + const flyoutIsExpandable = useFlyoutIsExpandable({ getFieldsData, dataAsNestedObject }); + const tabsDisplayed = useMemo(() => { + return flyoutIsExpandable + ? [tabs.overviewTab, tabs.tableTab, tabs.jsonTab] + : [tabs.tableTab, tabs.jsonTab]; + }, [flyoutIsExpandable]); const selectedTabId = useMemo(() => { const defaultTab = tabsDisplayed[0].id; @@ -49,7 +52,7 @@ export const RightPanel: FC> = memo(({ path }) => { return tabsDisplayed.map((tab) => tab.id).find((tabId) => tabId === path.tab) ?? defaultTab; }, [path, tabsDisplayed]); - const setSelectedTabId = (tabId: RightPanelTabsType[number]['id']) => { + const setSelectedTabId = (tabId: RightPanelTabType['id']) => { openRightPanel({ id: DocumentDetailsRightPanelKey, path: { @@ -78,7 +81,7 @@ export const RightPanel: FC> = memo(({ path }) => { return ( <> - + ; +} -/** - * Tabs to display in the document details expandable flyout right section - */ -export const tabs: RightPanelTabsType = [ - { - id: 'overview', - 'data-test-subj': OVERVIEW_TAB_TEST_ID, - name: ( - - ), - content: , - }, - { - id: 'table', - 'data-test-subj': TABLE_TAB_TEST_ID, - name: ( - - ), - content: , - }, - { - id: 'json', - 'data-test-subj': JSON_TAB_TEST_ID, - name: ( - - ), - content: , - }, -]; +export const overviewTab: RightPanelTabType = { + id: 'overview', + 'data-test-subj': OVERVIEW_TAB_TEST_ID, + name: ( + + ), + content: , +}; + +export const tableTab: RightPanelTabType = { + id: 'table', + 'data-test-subj': TABLE_TAB_TEST_ID, + name: ( + + ), + content: , +}; + +export const jsonTab: RightPanelTabType = { + id: 'json', + 'data-test-subj': JSON_TAB_TEST_ID, + name: ( + + ), + content: , +}; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/tabs/overview_tab.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/tabs/overview_tab.tsx index 8901777a68e7b..b6202a2568479 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/tabs/overview_tab.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/tabs/overview_tab.tsx @@ -26,9 +26,7 @@ export const OverviewTab: FC = memo(() => { paddingSize="none" aria-label={i18n.translate( 'xpack.securitySolution.flyout.right.overview.overviewContentAriaLabel', - { - defaultMessage: 'Alert overview', - } + { defaultMessage: 'Overview' } )} > diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/utils/event_utils.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/utils/event_utils.test.tsx new file mode 100644 index 0000000000000..a61ce667b9032 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/utils/event_utils.test.tsx @@ -0,0 +1,33 @@ +/* + * 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 { isEcsAllowedValue, getEcsAllowedValueDescription } from './event_utils'; + +describe('test isEcsAllowedValue', () => { + it('should return if the value is an allowed value given by field name', () => { + expect(isEcsAllowedValue('event.kind', 'event')).toBe(true); + expect(isEcsAllowedValue('event.kind', 'not ecs')).toBe(false); + expect(isEcsAllowedValue('event.category', 'not ecs')).toBe(false); + }); +}); + +describe('test getEcsAllowedValueDescription', () => { + it('should return correct description based on field', () => { + expect(getEcsAllowedValueDescription('event.kind', 'metric')).toBe( + 'This value is used to indicate that this event describes a numeric measurement taken at given point in time.\nExamples include CPU utilization, memory usage, or device temperature.\nMetric events are often collected on a predictable frequency, such as once every few seconds, or once a minute, but can also be used to describe ad-hoc numeric metric queries.' + ); + expect(getEcsAllowedValueDescription('event.category', 'malware')).toBe( + 'Malware detection events and alerts. Use this category to visualize and analyze malware detections from EDR/EPP systems such as Elastic Endpoint Security, Symantec Endpoint Protection, Crowdstrike, and network IDS/IPS systems such as Suricata, or other sources of malware-related events such as Palo Alto Networks threat logs and Wildfire logs.' + ); + + expect(getEcsAllowedValueDescription('event.kind', 'not ecs')).toBe( + 'This field is not an ecs field, description is not available.' + ); + expect(getEcsAllowedValueDescription('event.category', 'not ecs')).toBe( + 'This field is not an ecs field, description is not available.' + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/utils/event_utils.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/utils/event_utils.tsx new file mode 100644 index 0000000000000..3d76d58c3e3a8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/utils/event_utils.tsx @@ -0,0 +1,72 @@ +/* + * 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 { EcsFlat } from '@elastic/ecs'; +import { i18n } from '@kbn/i18n'; + +export interface AllowedValue { + description?: string; + expected_event_types?: string[]; + name?: string; +} + +type FieldName = 'event.kind' | 'event.category'; + +/** + * Helper function to return if the value is in the allowed value list of an ecs field + * @param fieldName + * @param value + * @returns boolean if value is an allowed value + */ +export const isEcsAllowedValue = ( + fieldName: FieldName, + value: string | undefined | null +): boolean => { + if (!value || value == null) { + return false; + } + const allowedValues: AllowedValue[] = EcsFlat[fieldName]?.allowed_values ?? []; + return Boolean(allowedValues?.find((item) => item.name === value)); +}; + +/** + * Helper function to return the description of an allowed value of the specified field + * @param fieldName + * @param value + * @returns ecs description of the value + */ +export const getEcsAllowedValueDescription = (fieldName: FieldName, value: string): string => { + const allowedValues: AllowedValue[] = EcsFlat[fieldName]?.allowed_values ?? []; + return ( + allowedValues?.find((item) => item.name === value)?.description ?? + i18n.translate('xpack.securitySolution.flyout.right.about.noEventKindDescriptionMessage', { + defaultMessage: 'This field is not an ecs field, description is not available.', + }) + ); +}; + +// mapping of event category to the field displayed as title +export const EVENT_CATEGORY_TO_FIELD: Record = { + authentication: 'user.name', + configuration: '', + database: '', + driver: '', + email: '', + file: 'file.name', + host: 'host.name', + iam: '', + intrusion_detection: '', + malware: '', + network: '', + package: '', + process: 'process.name', + registry: '', + session: '', + threat: '', + vulnerability: '', + web: '', +}; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/mocks/mock_get_fields_data.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/mocks/mock_get_fields_data.ts index a0cdb76ae05bb..e1f156ad8b531 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/mocks/mock_get_fields_data.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/mocks/mock_get_fields_data.ts @@ -11,6 +11,7 @@ import { ALERT_SEVERITY, ALERT_SUPPRESSION_DOCS_COUNT, } from '@kbn/rule-data-utils'; +import { EventKind } from '../constants/event_kinds'; const mockFieldData: Record = { [ALERT_SEVERITY]: ['low'], @@ -20,6 +21,7 @@ const mockFieldData: Record = { [ALERT_REASON]: ['reason'], [ALERT_SUPPRESSION_DOCS_COUNT]: ['1'], '@timestamp': ['2023-01-01T00:00:00.000Z'], + 'event.kind': [EventKind.signal], }; /** diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/utils.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/utils.test.tsx index ef2a14728bae7..531bc1b57df51 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/utils.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/utils.test.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { getField } from './utils'; +import { getField, getFieldArray } from './utils'; describe('test getField', () => { it('should return the string value if field is a string', () => { @@ -28,3 +28,22 @@ describe('test getField', () => { expect(getField(null, '-')).toBe('-'); }); }); + +describe('test getFieldArray', () => { + it('should return the string value in an array if field is a string', () => { + expect(getFieldArray('test string')).toStrictEqual(['test string']); + }); + it('should return field array', () => { + expect(getFieldArray(['string1', 'string2', 'string3'])).toStrictEqual([ + 'string1', + 'string2', + 'string3', + ]); + expect(getFieldArray([1, 2, 3])).toStrictEqual([1, 2, 3]); + }); + + it('should return empty array if field is null or empty', () => { + expect(getFieldArray(undefined)).toStrictEqual([]); + expect(getFieldArray(null)).toStrictEqual([]); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/utils.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/utils.tsx index 88f0c399d723d..72d568325676e 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/utils.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/utils.tsx @@ -19,3 +19,17 @@ export const getField = (field: unknown | unknown[], emptyValue?: string) => { } return emptyValue ?? null; }; + +/** + * Helper function to retrieve a field's value in an array + * @param field type unknown or unknown[] + * @return the field's value in an array + */ +export const getFieldArray = (field: unknown | unknown[]) => { + if (typeof field === 'string') { + return [field]; + } else if (Array.isArray(field) && field.length > 0) { + return field; + } + return []; +}; diff --git a/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_right_panel.ts b/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_right_panel.ts index afe87189acd37..77945869ff0a9 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_right_panel.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_right_panel.ts @@ -16,7 +16,7 @@ import { RISK_SCORE_VALUE_TEST_ID, SEVERITY_VALUE_TEST_ID, STATUS_BUTTON_TEST_ID, - FLYOUT_HEADER_TITLE_TEST_ID, + FLYOUT_ALERT_HEADER_TITLE_TEST_ID, ASSIGNEES_HEADER_TEST_ID, } from '@kbn/security-solution-plugin/public/flyout/document_details/right/components/test_ids'; import { @@ -32,13 +32,13 @@ export const DOCUMENT_DETAILS_FLYOUT_BODY = getDataTestSubjectSelector(FLYOUT_BO /* Header */ export const DOCUMENT_DETAILS_FLYOUT_HEADER_ICON = getDataTestSubjectSelector( - TITLE_LINK_ICON_TEST_ID(FLYOUT_HEADER_TITLE_TEST_ID) + TITLE_LINK_ICON_TEST_ID(FLYOUT_ALERT_HEADER_TITLE_TEST_ID) ); export const DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE = getDataTestSubjectSelector( - TITLE_HEADER_TEXT_TEST_ID(FLYOUT_HEADER_TITLE_TEST_ID) + TITLE_HEADER_TEXT_TEST_ID(FLYOUT_ALERT_HEADER_TITLE_TEST_ID) ); export const DOCUMENT_DETAILS_FLYOUT_HEADER_LINK_ICON = getDataTestSubjectSelector( - TITLE_LINK_ICON_TEST_ID(FLYOUT_HEADER_TITLE_TEST_ID) + TITLE_LINK_ICON_TEST_ID(FLYOUT_ALERT_HEADER_TITLE_TEST_ID) ); export const DOCUMENT_DETAILS_FLYOUT_CLOSE_BUTTON = getDataTestSubjectSelector('euiFlyoutCloseButton'); diff --git a/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_right_panel_overview_tab.ts b/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_right_panel_overview_tab.ts index e7095ac71b236..698066fd06056 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_right_panel_overview_tab.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_right_panel_overview_tab.ts @@ -11,8 +11,8 @@ import { } from '@kbn/security-solution-plugin/public/flyout/shared/components/test_ids'; import { ABOUT_SECTION_HEADER_TEST_ID, - DESCRIPTION_DETAILS_TEST_ID, - DESCRIPTION_TITLE_TEST_ID, + ALERT_DESCRIPTION_DETAILS_TEST_ID, + ALERT_DESCRIPTION_TITLE_TEST_ID, RULE_SUMMARY_BUTTON_TEST_ID, HIGHLIGHTED_FIELDS_DETAILS_TEST_ID, HIGHLIGHTED_FIELDS_TITLE_TEST_ID, @@ -48,10 +48,11 @@ import { getDataTestSubjectSelector } from '../../helpers/common'; export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ABOUT_SECTION_HEADER = getDataTestSubjectSelector( ABOUT_SECTION_HEADER_TEST_ID ); -export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_TITLE = - getDataTestSubjectSelector(DESCRIPTION_TITLE_TEST_ID); +export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_TITLE = getDataTestSubjectSelector( + ALERT_DESCRIPTION_TITLE_TEST_ID +); export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_DETAILS = getDataTestSubjectSelector( - DESCRIPTION_DETAILS_TEST_ID + ALERT_DESCRIPTION_DETAILS_TEST_ID ); export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_OPEN_RULE_PREVIEW_BUTTON = getDataTestSubjectSelector(RULE_SUMMARY_BUTTON_TEST_ID);