diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/correlations_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/correlations_overview.test.tsx index f4a97b7deed11..71292b73a68e0 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/correlations_overview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/correlations_overview.test.tsx @@ -22,7 +22,8 @@ import { CORRELATIONS_RELATED_CASES_TEST_ID, CORRELATIONS_SUPPRESSED_ALERTS_TEST_ID, CORRELATIONS_TEST_ID, - SUMMARY_ROW_VALUE_TEST_ID, + SUMMARY_ROW_BUTTON_TEST_ID, + SUMMARY_ROW_TEXT_TEST_ID, } from './test_ids'; import { useShowRelatedAlertsByAncestry } from '../../shared/hooks/use_show_related_alerts_by_ancestry'; import { useShowRelatedAlertsBySameSourceEvent } from '../../shared/hooks/use_show_related_alerts_by_same_source_event'; @@ -58,17 +59,32 @@ const TITLE_LINK_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(CORRELATIO const TITLE_ICON_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(CORRELATIONS_TEST_ID); const TITLE_TEXT_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(CORRELATIONS_TEST_ID); -const SUPPRESSED_ALERTS_TEST_ID = SUMMARY_ROW_VALUE_TEST_ID(CORRELATIONS_SUPPRESSED_ALERTS_TEST_ID); -const RELATED_ALERTS_BY_ANCESTRY_TEST_ID = SUMMARY_ROW_VALUE_TEST_ID( +const SUPPRESSED_ALERTS_TEXT_TEST_ID = SUMMARY_ROW_TEXT_TEST_ID( + CORRELATIONS_SUPPRESSED_ALERTS_TEST_ID +); +const SUPPRESSED_ALERTS_VALUE_TEST_ID = SUMMARY_ROW_BUTTON_TEST_ID( + CORRELATIONS_SUPPRESSED_ALERTS_TEST_ID +); +const RELATED_ALERTS_BY_ANCESTRY_TEXT_TEST_ID = SUMMARY_ROW_TEXT_TEST_ID( + CORRELATIONS_RELATED_ALERTS_BY_ANCESTRY_TEST_ID +); +const RELATED_ALERTS_BY_ANCESTRY_VALUE_TEST_ID = SUMMARY_ROW_BUTTON_TEST_ID( CORRELATIONS_RELATED_ALERTS_BY_ANCESTRY_TEST_ID ); -const RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEST_ID = SUMMARY_ROW_VALUE_TEST_ID( +const RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEXT_TEST_ID = SUMMARY_ROW_TEXT_TEST_ID( CORRELATIONS_RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEST_ID ); -const RELATED_ALERTS_BY_SESSION_TEST_ID = SUMMARY_ROW_VALUE_TEST_ID( +const RELATED_ALERTS_BY_SAME_SOURCE_EVENT_VALUE_TEST_ID = SUMMARY_ROW_BUTTON_TEST_ID( + CORRELATIONS_RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEST_ID +); +const RELATED_ALERTS_BY_SESSION_TEXT_TEST_ID = SUMMARY_ROW_TEXT_TEST_ID( + CORRELATIONS_RELATED_ALERTS_BY_SESSION_TEST_ID +); +const RELATED_ALERTS_BY_SESSION_VALUE_TEST_ID = SUMMARY_ROW_BUTTON_TEST_ID( CORRELATIONS_RELATED_ALERTS_BY_SESSION_TEST_ID ); -const RELATED_CASES_TEST_ID = SUMMARY_ROW_VALUE_TEST_ID(CORRELATIONS_RELATED_CASES_TEST_ID); +const RELATED_CASES_TEXT_TEST_ID = SUMMARY_ROW_TEXT_TEST_ID(CORRELATIONS_RELATED_CASES_TEST_ID); +const RELATED_CASES_VALUE_TEST_ID = SUMMARY_ROW_BUTTON_TEST_ID(CORRELATIONS_RELATED_CASES_TEST_ID); const panelContextValue = { eventId: 'event id', @@ -193,11 +209,16 @@ describe('', () => { }); const { getByTestId, queryByText } = render(renderCorrelationsOverview(panelContextValue)); - expect(getByTestId(RELATED_ALERTS_BY_ANCESTRY_TEST_ID)).toBeInTheDocument(); - expect(getByTestId(RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEST_ID)).toBeInTheDocument(); - expect(getByTestId(RELATED_ALERTS_BY_SESSION_TEST_ID)).toBeInTheDocument(); - expect(getByTestId(RELATED_CASES_TEST_ID)).toBeInTheDocument(); - expect(getByTestId(SUPPRESSED_ALERTS_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(RELATED_ALERTS_BY_ANCESTRY_TEXT_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(RELATED_ALERTS_BY_ANCESTRY_VALUE_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEXT_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(RELATED_ALERTS_BY_SAME_SOURCE_EVENT_VALUE_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(RELATED_ALERTS_BY_SESSION_TEXT_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(RELATED_ALERTS_BY_SESSION_VALUE_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(RELATED_CASES_TEXT_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(RELATED_CASES_VALUE_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(SUPPRESSED_ALERTS_TEXT_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(SUPPRESSED_ALERTS_VALUE_TEST_ID)).toBeInTheDocument(); expect(queryByText(NO_DATA_MESSAGE)).not.toBeInTheDocument(); }); @@ -215,11 +236,18 @@ describe('', () => { jest.mocked(useShowSuppressedAlerts).mockReturnValue({ show: false, alertSuppressionCount: 0 }); const { getByText, queryByTestId } = render(renderCorrelationsOverview(panelContextValue)); - expect(queryByTestId(RELATED_ALERTS_BY_ANCESTRY_TEST_ID)).not.toBeInTheDocument(); - expect(queryByTestId(RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEST_ID)).not.toBeInTheDocument(); - expect(queryByTestId(RELATED_ALERTS_BY_SESSION_TEST_ID)).not.toBeInTheDocument(); - expect(queryByTestId(RELATED_CASES_TEST_ID)).not.toBeInTheDocument(); - expect(queryByTestId(SUPPRESSED_ALERTS_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(RELATED_ALERTS_BY_ANCESTRY_TEXT_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(RELATED_ALERTS_BY_ANCESTRY_VALUE_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEXT_TEST_ID)).not.toBeInTheDocument(); + expect( + queryByTestId(RELATED_ALERTS_BY_SAME_SOURCE_EVENT_VALUE_TEST_ID) + ).not.toBeInTheDocument(); + expect(queryByTestId(RELATED_ALERTS_BY_SESSION_TEXT_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(RELATED_ALERTS_BY_SESSION_VALUE_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(RELATED_CASES_TEXT_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(RELATED_CASES_VALUE_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(SUPPRESSED_ALERTS_TEXT_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(SUPPRESSED_ALERTS_VALUE_TEST_ID)).not.toBeInTheDocument(); expect(getByText(NO_DATA_MESSAGE)).toBeInTheDocument(); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/correlations_overview.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/correlations_overview.tsx index c2494b4cde675..9ab130cfc2de1 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/correlations_overview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/correlations_overview.tsx @@ -134,7 +134,7 @@ export const CorrelationsOverview: React.FC = () => { data-test-subj={CORRELATIONS_TEST_ID} > {canShowAtLeastOneInsight ? ( - + {showSuppressedAlerts && ( )} diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_summary_row.stories.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_summary_row.stories.tsx deleted file mode 100644 index eb76108d6b215..0000000000000 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_summary_row.stories.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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 type { Story } from '@storybook/react'; -import { css } from '@emotion/react'; -import { InsightsSummaryRow } from './insights_summary_row'; - -export default { - component: InsightsSummaryRow, - title: 'Flyout/InsightsSummaryRow', -}; - -const wrapper = (children: React.ReactNode) => ( -
- {children} -
-); - -export const Default: Story = () => - wrapper( - - ); - -export const InvalidColor: Story = () => - wrapper( - - ); - -export const NoColor: Story = () => - wrapper(); - -export const LongText: Story = () => - wrapper( - - ); - -export const LongNumber: Story = () => - wrapper( - - ); - -export const Loading: Story = () => - wrapper(); - -export const Error: Story = () => - wrapper(); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_summary_row.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_summary_row.test.tsx index 3e10e83332a97..2a721e317781e 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_summary_row.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_summary_row.test.tsx @@ -9,74 +9,147 @@ import React from 'react'; import { render } from '@testing-library/react'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { InsightsSummaryRow } from './insights_summary_row'; +import { useDocumentDetailsContext } from '../../shared/context'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { DocumentDetailsLeftPanelKey } from '../../shared/constants/panel_keys'; +import { LeftPanelInsightsTab } from '../../left'; + +jest.mock('@kbn/expandable-flyout'); +jest.mock('../../shared/context'); + +const mockOpenLeftPanel = jest.fn(); +const scopeId = 'scopeId'; +const eventId = 'eventId'; +const indexName = 'indexName'; const testId = 'test'; -const iconTestId = `${testId}Icon`; +const textTestId = `${testId}Text`; +const buttonTestId = `${testId}Button`; const valueTestId = `${testId}Value`; -const colorTestId = `${testId}Color`; const loadingTestId = `${testId}Loading`; describe('', () => { - it('should render by default', () => { + beforeEach(() => { + jest.clearAllMocks(); + + (useDocumentDetailsContext as jest.Mock).mockReturnValue({ + eventId, + indexName, + scopeId, + isPreviewMode: false, + }); + (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ openLeftPanel: mockOpenLeftPanel }); + }); + + it('should render loading skeleton if loading is true', () => { const { getByTestId } = render( + {'value for this'}} + data-test-subj={testId} + /> + ); + + expect(getByTestId(loadingTestId)).toBeInTheDocument(); + }); + + it('should only render null when error is true', () => { + const { container } = render( + {'value for this'}} /> + ); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should render the value component', () => { + const { getByTestId, queryByTestId } = render( {'value for this'}} data-test-subj={testId} /> ); - expect(getByTestId(iconTestId)).toBeInTheDocument(); - expect(getByTestId(valueTestId)).toHaveTextContent('1 this is a test for red'); - expect(getByTestId(colorTestId)).toBeInTheDocument(); + expect(getByTestId(textTestId)).toHaveTextContent('this is a test for red'); + expect(getByTestId(valueTestId)).toHaveTextContent('value for this'); + expect(queryByTestId(buttonTestId)).not.toBeInTheDocument(); }); - it('should render loading skeletton if loading is true', () => { - const { getByTestId } = render( - + it('should render the value as EuiBadge and EuiButtonEmpty', () => { + const { getByTestId, queryByTestId } = render( + + + ); - expect(getByTestId(loadingTestId)).toBeInTheDocument(); + expect(getByTestId(textTestId)).toHaveTextContent('this is a test for red'); + expect(getByTestId(buttonTestId)).toHaveTextContent('2'); + expect(queryByTestId(valueTestId)).not.toBeInTheDocument(); }); - it('should only render null when error is true', () => { - const { container } = render(); + it('should render big numbers formatted correctly', () => { + const { getByTestId } = render( + + + + ); - expect(container).toBeEmptyDOMElement(); + expect(getByTestId(buttonTestId)).toHaveTextContent('2k'); }); - it('should handle big number in a compact notation', () => { + it('should open the expanded section to the correct tab when the number is clicked', () => { const { getByTestId } = render( ); + getByTestId(buttonTestId).click(); - expect(getByTestId(valueTestId)).toHaveTextContent('160k this is a test for red'); + expect(mockOpenLeftPanel).toHaveBeenCalledWith({ + id: DocumentDetailsLeftPanelKey, + path: { + tab: LeftPanelInsightsTab, + subTab: 'subTab', + }, + params: { + id: eventId, + indexName, + scopeId, + }, + }); }); - it(`should not show the colored dot if color isn't provided`, () => { - const { queryByTestId } = render( + it('should disabled the click when in preview mode', () => { + (useDocumentDetailsContext as jest.Mock).mockReturnValue({ + eventId, + indexName, + scopeId, + isPreviewMode: true, + }); + + const { getByTestId } = render( ); + const button = getByTestId(buttonTestId); + + expect(button).toHaveAttribute('disabled'); - expect(queryByTestId(colorTestId)).not.toBeInTheDocument(); + button.click(); + expect(mockOpenLeftPanel).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_summary_row.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_summary_row.tsx index 23f838f5068bb..56a19d2eca965 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_summary_row.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_summary_row.tsx @@ -6,19 +6,25 @@ */ import type { ReactElement, VFC } from 'react'; -import React from 'react'; +import React, { useMemo, useCallback } from 'react'; import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; -import { - EuiIcon, - EuiFlexGroup, - EuiFlexItem, - EuiHealth, - EuiSkeletonText, - useEuiTheme, -} from '@elastic/eui'; +import { EuiBadge, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSkeletonText } from '@elastic/eui'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { useDocumentDetailsContext } from '../../shared/context'; +import { DocumentDetailsLeftPanelKey } from '../../shared/constants/panel_keys'; +import { LeftPanelInsightsTab } from '../../left'; import { FormattedCount } from '../../../../common/components/formatted_number'; +const LOADING = i18n.translate( + 'xpack.securitySolution.flyout.right.insights.insightSummaryLoadingAriaLabel', + { defaultMessage: 'Loading' } +); +const BUTTON = i18n.translate( + 'xpack.securitySolution.flyout.right.insights.insightSummaryButtonAriaLabel', + { defaultMessage: 'Click to see more details' } +); + export interface InsightsSummaryRowProps { /** * Optional parameter used to display a loading spinner @@ -29,22 +35,17 @@ export interface InsightsSummaryRowProps { */ error?: boolean; /** - * Icon to display on the left side of each row + * Text corresponding of the number of results/entries */ - icon: string; + text: string | ReactElement; /** * Number of results/entries found */ - value?: number; + value: number | ReactElement; /** - * Text corresponding of the number of results/entries + * Optional parameter used to know which subtab to navigate to when the user clicks on the button */ - text: string | ReactElement; - /** - * Optional parameter for now, will be used to display a dot on the right side - * (corresponding to some sort of severity?) - */ - color?: string; // TODO remove optional when we have guidance on what the colors will actually be + expandedSubTab?: string; /** * Prefix data-test-subj because this component will be used in multiple places */ @@ -52,35 +53,73 @@ export interface InsightsSummaryRowProps { } /** - * Panel showing summary information as an icon, a count and text as well as a severity colored dot. + * Panel showing summary information. + * The default display is a text on the left and a count on the right, displayed with a clickable EuiBadge. + * The left and right section can accept a ReactElement to allow for more complex display. * Should be used for Entities, Threat intelligence, Prevalence, Correlations and Results components under the Insights section. - * The colored dot is currently optional but will ultimately be mandatory (waiting on PM and UIUX). */ export const InsightsSummaryRow: VFC = ({ loading = false, error = false, - icon, value, text, - color, + expandedSubTab, 'data-test-subj': dataTestSubj, }) => { - const { euiTheme } = useEuiTheme(); + const { eventId, indexName, scopeId, isPreviewMode } = useDocumentDetailsContext(); + const { openLeftPanel } = useExpandableFlyoutApi(); + + const onClick = useCallback(() => { + openLeftPanel({ + id: DocumentDetailsLeftPanelKey, + path: { + tab: LeftPanelInsightsTab, + subTab: expandedSubTab, + }, + params: { + id: eventId, + indexName, + scopeId, + }, + }); + }, [eventId, expandedSubTab, indexName, openLeftPanel, scopeId]); + + const textDataTestSubj = useMemo(() => `${dataTestSubj}Text`, [dataTestSubj]); + const loadingDataTestSubj = useMemo(() => `${dataTestSubj}Loading`, [dataTestSubj]); + + const button = useMemo(() => { + const buttonDataTestSubj = `${dataTestSubj}Button`; + const valueDataTestSubj = `${dataTestSubj}Value`; + + return ( + <> + {typeof value === 'number' ? ( + + + + + + ) : ( +
{value}
+ )} + + ); + }, [dataTestSubj, isPreviewMode, onClick, value]); - const loadingDataTestSubj = `${dataTestSubj}Loading`; if (loading) { return ( ); @@ -90,10 +129,6 @@ export const InsightsSummaryRow: VFC = ({ return null; } - const iconDataTestSubj = `${dataTestSubj}Icon`; - const valueDataTestSubj = `${dataTestSubj}Value`; - const colorDataTestSubj = `${dataTestSubj}Color`; - return ( = ({ alignItems={'center'} responsive={false} > - - - = ({ overflow: hidden; `} > - {value && } {text} + {text} - {color && ( - - - - )} + {button} ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/prevalence_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/prevalence_overview.test.tsx index a47ed04c85b5a..527b9830b3948 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/prevalence_overview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/prevalence_overview.test.tsx @@ -8,7 +8,11 @@ import { render } from '@testing-library/react'; import { TestProviders } from '../../../../common/mock'; import { DocumentDetailsContext } from '../../shared/context'; -import { PREVALENCE_TEST_ID } from './test_ids'; +import { + PREVALENCE_TEST_ID, + SUMMARY_ROW_TEXT_TEST_ID, + SUMMARY_ROW_VALUE_TEST_ID, +} from './test_ids'; import { DocumentDetailsLeftPanelKey } from '../../shared/constants/panel_keys'; import { LeftPanelInsightsTab } from '../../left'; import React from 'react'; @@ -149,21 +153,19 @@ describe('', () => { expect(getByTestId(TITLE_LINK_TEST_ID)).toHaveTextContent('Prevalence'); - const iconDataTestSubj1 = `${PREVALENCE_TEST_ID}${field1}Icon`; - const valueDataTestSubj1 = `${PREVALENCE_TEST_ID}${field1}Value`; - expect(getByTestId(iconDataTestSubj1)).toBeInTheDocument(); - expect(getByTestId(valueDataTestSubj1)).toBeInTheDocument(); - expect(getByTestId(valueDataTestSubj1)).toHaveTextContent('field1, value1 is uncommon'); - - const iconDataTestSubj2 = `${PREVALENCE_TEST_ID}${field2}Icon`; - const valueDataTestSubj2 = `${PREVALENCE_TEST_ID}${field2}Value`; - expect(getByTestId(iconDataTestSubj2)).toBeInTheDocument(); - expect(getByTestId(valueDataTestSubj2)).toBeInTheDocument(); - expect(getByTestId(valueDataTestSubj2)).toHaveTextContent('field2, value2,value22 is uncommon'); - - const iconDataTestSubj3 = `${PREVALENCE_TEST_ID}${field3}Icon`; - const valueDataTestSubj3 = `${PREVALENCE_TEST_ID}${field3}Value`; - expect(queryByTestId(iconDataTestSubj3)).not.toBeInTheDocument(); + const textDataTestSubj1 = SUMMARY_ROW_TEXT_TEST_ID(`${PREVALENCE_TEST_ID}${field1}`); + const valueDataTestSubj1 = SUMMARY_ROW_VALUE_TEST_ID(`${PREVALENCE_TEST_ID}${field1}`); + expect(getByTestId(textDataTestSubj1)).toHaveTextContent('field1, value1'); + expect(getByTestId(valueDataTestSubj1)).toHaveTextContent('Uncommon'); + + const textDataTestSubj2 = SUMMARY_ROW_TEXT_TEST_ID(`${PREVALENCE_TEST_ID}${field2}`); + const valueDataTestSubj2 = SUMMARY_ROW_VALUE_TEST_ID(`${PREVALENCE_TEST_ID}${field2}`); + expect(getByTestId(textDataTestSubj2)).toHaveTextContent('field2, value2'); + expect(getByTestId(valueDataTestSubj2)).toHaveTextContent('Uncommon'); + + const textDataTestSubj3 = SUMMARY_ROW_TEXT_TEST_ID(`${PREVALENCE_TEST_ID}${field3}`); + const valueDataTestSubj3 = SUMMARY_ROW_VALUE_TEST_ID(`${PREVALENCE_TEST_ID}${field3}`); + expect(queryByTestId(textDataTestSubj3)).not.toBeInTheDocument(); expect(queryByTestId(valueDataTestSubj3)).not.toBeInTheDocument(); expect(queryByText(NO_DATA_MESSAGE)).not.toBeInTheDocument(); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/prevalence_overview.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/prevalence_overview.tsx index 96ee603607742..adb660f67ce72 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/prevalence_overview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/prevalence_overview.tsx @@ -7,7 +7,7 @@ import type { FC } from 'react'; import React, { useCallback, useMemo } from 'react'; -import { EuiFlexGroup } from '@elastic/eui'; +import { EuiBadge, EuiFlexGroup } from '@elastic/eui'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { FormattedMessage } from '@kbn/i18n-react'; import { ExpandablePanel } from '@kbn/security-solution-common'; @@ -19,6 +19,13 @@ import { LeftPanelInsightsTab } from '../../left'; import { PREVALENCE_TAB_ID } from '../../left/components/prevalence_details'; import { InsightsSummaryRow } from './insights_summary_row'; +const UNCOMMON = ( + +); + const PERCENTAGE_THRESHOLD = 0.1; // we show the prevalence if its value is below 10% const DEFAULT_FROM = 'now-30d'; const DEFAULT_TO = 'now'; @@ -104,18 +111,17 @@ export const PrevalenceOverview: FC = () => { content={{ loading, error }} data-test-subj={PREVALENCE_TEST_ID} > - + {uncommonData.length > 0 ? ( uncommonData.map((d) => ( + <> + {d.field} + {','} {d.values.toString()} + } + value={{UNCOMMON}} data-test-subj={`${PREVALENCE_TEST_ID}${d.field}`} key={`${PREVALENCE_TEST_ID}${d.field}`} /> diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_alerts_by_ancestry.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_alerts_by_ancestry.test.tsx index 5aad641c6e400..38efe27b16ea9 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_alerts_by_ancestry.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_alerts_by_ancestry.test.tsx @@ -9,22 +9,32 @@ import React from 'react'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { render } from '@testing-library/react'; import { - SUMMARY_ROW_ICON_TEST_ID, - SUMMARY_ROW_VALUE_TEST_ID, + SUMMARY_ROW_TEXT_TEST_ID, SUMMARY_ROW_LOADING_TEST_ID, CORRELATIONS_RELATED_ALERTS_BY_ANCESTRY_TEST_ID, + SUMMARY_ROW_BUTTON_TEST_ID, } from './test_ids'; import { RelatedAlertsByAncestry } from './related_alerts_by_ancestry'; import { useFetchRelatedAlertsByAncestry } from '../../shared/hooks/use_fetch_related_alerts_by_ancestry'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { DocumentDetailsLeftPanelKey } from '../../shared/constants/panel_keys'; +import { LeftPanelInsightsTab } from '../../left'; +import { CORRELATIONS_TAB_ID } from '../../left/components/correlations_details'; +import { useDocumentDetailsContext } from '../../shared/context'; +jest.mock('@kbn/expandable-flyout'); +jest.mock('../../shared/context'); jest.mock('../../shared/hooks/use_fetch_related_alerts_by_ancestry'); +const mockOpenLeftPanel = jest.fn(); const documentId = 'documentId'; const indices = ['indices']; const scopeId = 'scopeId'; +const eventId = 'eventId'; +const indexName = 'indexName'; -const ICON_TEST_ID = SUMMARY_ROW_ICON_TEST_ID(CORRELATIONS_RELATED_ALERTS_BY_ANCESTRY_TEST_ID); -const VALUE_TEST_ID = SUMMARY_ROW_VALUE_TEST_ID(CORRELATIONS_RELATED_ALERTS_BY_ANCESTRY_TEST_ID); +const TEXT_TEST_ID = SUMMARY_ROW_TEXT_TEST_ID(CORRELATIONS_RELATED_ALERTS_BY_ANCESTRY_TEST_ID); +const BUTTON_TEST_ID = SUMMARY_ROW_BUTTON_TEST_ID(CORRELATIONS_RELATED_ALERTS_BY_ANCESTRY_TEST_ID); const LOADING_TEST_ID = SUMMARY_ROW_LOADING_TEST_ID( CORRELATIONS_RELATED_ALERTS_BY_ANCESTRY_TEST_ID ); @@ -37,34 +47,40 @@ const renderRelatedAlertsByAncestry = () => ); describe('', () => { - it('should render many related alerts correctly', () => { + beforeEach(() => { + jest.clearAllMocks(); + + (useDocumentDetailsContext as jest.Mock).mockReturnValue({ + eventId, + indexName, + scopeId, + isPreviewMode: false, + }); + (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ openLeftPanel: mockOpenLeftPanel }); + }); + + it('should render single related alert correctly', () => { (useFetchRelatedAlertsByAncestry as jest.Mock).mockReturnValue({ loading: false, error: false, - dataCount: 2, + dataCount: 1, }); const { getByTestId } = renderRelatedAlertsByAncestry(); - expect(getByTestId(ICON_TEST_ID)).toBeInTheDocument(); - const value = getByTestId(VALUE_TEST_ID); - expect(value).toBeInTheDocument(); - expect(value).toHaveTextContent('2 alerts related by ancestry'); - expect(getByTestId(VALUE_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(TEXT_TEST_ID)).toHaveTextContent('Alert related by ancestry'); + expect(getByTestId(BUTTON_TEST_ID)).toHaveTextContent('1'); }); - it('should render single related alerts correctly', () => { + it('should render multiple related alerts correctly', () => { (useFetchRelatedAlertsByAncestry as jest.Mock).mockReturnValue({ loading: false, error: false, - dataCount: 1, + dataCount: 2, }); const { getByTestId } = renderRelatedAlertsByAncestry(); - expect(getByTestId(ICON_TEST_ID)).toBeInTheDocument(); - const value = getByTestId(VALUE_TEST_ID); - expect(value).toBeInTheDocument(); - expect(value).toHaveTextContent('1 alert related by ancestry'); - expect(getByTestId(VALUE_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(TEXT_TEST_ID)).toHaveTextContent('Alerts related by ancestry'); + expect(getByTestId(BUTTON_TEST_ID)).toHaveTextContent('2'); }); it('should render loading skeleton', () => { @@ -85,4 +101,28 @@ describe('', () => { const { container } = renderRelatedAlertsByAncestry(); expect(container).toBeEmptyDOMElement(); }); + + it('should open the expanded section to the correct tab when the number is clicked', () => { + (useFetchRelatedAlertsByAncestry as jest.Mock).mockReturnValue({ + loading: false, + error: false, + dataCount: 1, + }); + + const { getByTestId } = renderRelatedAlertsByAncestry(); + getByTestId(BUTTON_TEST_ID).click(); + + expect(mockOpenLeftPanel).toHaveBeenCalledWith({ + id: DocumentDetailsLeftPanelKey, + path: { + tab: LeftPanelInsightsTab, + subTab: CORRELATIONS_TAB_ID, + }, + params: { + id: eventId, + indexName, + scopeId, + }, + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_alerts_by_ancestry.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_alerts_by_ancestry.tsx index 2e628ba61a7be..4b225d5595883 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_alerts_by_ancestry.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_alerts_by_ancestry.tsx @@ -5,14 +5,13 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; +import { CORRELATIONS_TAB_ID } from '../../left/components/correlations_details'; import { useFetchRelatedAlertsByAncestry } from '../../shared/hooks/use_fetch_related_alerts_by_ancestry'; import { InsightsSummaryRow } from './insights_summary_row'; import { CORRELATIONS_RELATED_ALERTS_BY_ANCESTRY_TEST_ID } from './test_ids'; -const ICON = 'warning'; - export interface RelatedAlertsByAncestryProps { /** * Id of the document @@ -41,21 +40,25 @@ export const RelatedAlertsByAncestry: React.VFC = indices, scopeId, }); - const text = ( - + + const text = useMemo( + () => ( + + ), + [dataCount] ); return ( diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_alerts_by_same_source_event.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_alerts_by_same_source_event.test.tsx index d52d547397789..80e7c99a60917 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_alerts_by_same_source_event.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_alerts_by_same_source_event.test.tsx @@ -9,23 +9,33 @@ import React from 'react'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { render } from '@testing-library/react'; import { - SUMMARY_ROW_ICON_TEST_ID, - SUMMARY_ROW_VALUE_TEST_ID, + SUMMARY_ROW_TEXT_TEST_ID, SUMMARY_ROW_LOADING_TEST_ID, CORRELATIONS_RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEST_ID, + SUMMARY_ROW_BUTTON_TEST_ID, } from './test_ids'; import { useFetchRelatedAlertsBySameSourceEvent } from '../../shared/hooks/use_fetch_related_alerts_by_same_source_event'; import { RelatedAlertsBySameSourceEvent } from './related_alerts_by_same_source_event'; +import { DocumentDetailsLeftPanelKey } from '../../shared/constants/panel_keys'; +import { LeftPanelInsightsTab } from '../../left'; +import { CORRELATIONS_TAB_ID } from '../../left/components/correlations_details'; +import { useDocumentDetailsContext } from '../../shared/context'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +jest.mock('@kbn/expandable-flyout'); +jest.mock('../../shared/context'); jest.mock('../../shared/hooks/use_fetch_related_alerts_by_same_source_event'); +const mockOpenLeftPanel = jest.fn(); const originalEventId = 'originalEventId'; const scopeId = 'scopeId'; +const eventId = 'eventId'; +const indexName = 'indexName'; -const ICON_TEST_ID = SUMMARY_ROW_ICON_TEST_ID( +const TEXT_TEST_ID = SUMMARY_ROW_TEXT_TEST_ID( CORRELATIONS_RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEST_ID ); -const VALUE_TEST_ID = SUMMARY_ROW_VALUE_TEST_ID( +const BUTTON_TEST_ID = SUMMARY_ROW_BUTTON_TEST_ID( CORRELATIONS_RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEST_ID ); const LOADING_TEST_ID = SUMMARY_ROW_LOADING_TEST_ID( @@ -40,34 +50,40 @@ const renderRelatedAlertsBySameSourceEvent = () => ); describe('', () => { - it('should render many related alerts correctly', () => { + beforeEach(() => { + jest.clearAllMocks(); + + (useDocumentDetailsContext as jest.Mock).mockReturnValue({ + eventId, + indexName, + scopeId, + isPreviewMode: false, + }); + (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ openLeftPanel: mockOpenLeftPanel }); + }); + + it('should render single related alert correctly', () => { (useFetchRelatedAlertsBySameSourceEvent as jest.Mock).mockReturnValue({ loading: false, error: false, - dataCount: 2, + dataCount: 1, }); const { getByTestId } = renderRelatedAlertsBySameSourceEvent(); - expect(getByTestId(ICON_TEST_ID)).toBeInTheDocument(); - const value = getByTestId(VALUE_TEST_ID); - expect(value).toBeInTheDocument(); - expect(value).toHaveTextContent('2 alerts related by source event'); - expect(getByTestId(VALUE_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(TEXT_TEST_ID)).toHaveTextContent('Alert related by source event'); + expect(getByTestId(BUTTON_TEST_ID)).toHaveTextContent('1'); }); - it('should render single related alerts correctly', () => { + it('should render multiple related alerts correctly', () => { (useFetchRelatedAlertsBySameSourceEvent as jest.Mock).mockReturnValue({ loading: false, error: false, - dataCount: 1, + dataCount: 2, }); const { getByTestId } = renderRelatedAlertsBySameSourceEvent(); - expect(getByTestId(ICON_TEST_ID)).toBeInTheDocument(); - const value = getByTestId(VALUE_TEST_ID); - expect(value).toBeInTheDocument(); - expect(value).toHaveTextContent('1 alert related by source event'); - expect(getByTestId(VALUE_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(TEXT_TEST_ID)).toHaveTextContent('Alerts related by source event'); + expect(getByTestId(BUTTON_TEST_ID)).toHaveTextContent('2'); }); it('should render loading skeleton', () => { @@ -87,10 +103,31 @@ describe('', () => { }); const { getByTestId } = renderRelatedAlertsBySameSourceEvent(); - expect(getByTestId(ICON_TEST_ID)).toBeInTheDocument(); - const value = getByTestId(VALUE_TEST_ID); - expect(value).toBeInTheDocument(); - expect(value).toHaveTextContent('0 alerts related by source event'); - expect(getByTestId(VALUE_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(TEXT_TEST_ID)).toHaveTextContent('Alerts related by source event'); + expect(getByTestId(BUTTON_TEST_ID)).toHaveTextContent('0'); + }); + + it('should open the expanded section to the correct tab when the number is clicked', () => { + (useFetchRelatedAlertsBySameSourceEvent as jest.Mock).mockReturnValue({ + loading: false, + error: true, + dataCount: 1, + }); + + const { getByTestId } = renderRelatedAlertsBySameSourceEvent(); + getByTestId(BUTTON_TEST_ID).click(); + + expect(mockOpenLeftPanel).toHaveBeenCalledWith({ + id: DocumentDetailsLeftPanelKey, + path: { + tab: LeftPanelInsightsTab, + subTab: CORRELATIONS_TAB_ID, + }, + params: { + id: eventId, + indexName, + scopeId, + }, + }); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_alerts_by_same_source_event.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_alerts_by_same_source_event.tsx index 0c1550dbb8692..dade35ca75546 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_alerts_by_same_source_event.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_alerts_by_same_source_event.tsx @@ -5,13 +5,12 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useFetchRelatedAlertsBySameSourceEvent } from '../../shared/hooks/use_fetch_related_alerts_by_same_source_event'; -import { InsightsSummaryRow } from './insights_summary_row'; +import { CORRELATIONS_TAB_ID } from '../../left/components/correlations_details'; import { CORRELATIONS_RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEST_ID } from './test_ids'; - -const ICON = 'warning'; +import { InsightsSummaryRow } from './insights_summary_row'; +import { useFetchRelatedAlertsBySameSourceEvent } from '../../shared/hooks/use_fetch_related_alerts_by_same_source_event'; export interface RelatedAlertsBySameSourceEventProps { /** @@ -35,20 +34,24 @@ export const RelatedAlertsBySameSourceEvent: React.VFC + + const text = useMemo( + () => ( + + ), + [dataCount] ); return ( diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_alerts_by_session.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_alerts_by_session.test.tsx index 96ab397229420..4aeeef1feb8b1 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_alerts_by_session.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_alerts_by_session.test.tsx @@ -9,21 +9,31 @@ import React from 'react'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { render } from '@testing-library/react'; import { - SUMMARY_ROW_ICON_TEST_ID, - SUMMARY_ROW_VALUE_TEST_ID, + SUMMARY_ROW_TEXT_TEST_ID, SUMMARY_ROW_LOADING_TEST_ID, CORRELATIONS_RELATED_ALERTS_BY_SESSION_TEST_ID, + SUMMARY_ROW_BUTTON_TEST_ID, } from './test_ids'; import { RelatedAlertsBySession } from './related_alerts_by_session'; import { useFetchRelatedAlertsBySession } from '../../shared/hooks/use_fetch_related_alerts_by_session'; +import { DocumentDetailsLeftPanelKey } from '../../shared/constants/panel_keys'; +import { LeftPanelInsightsTab } from '../../left'; +import { CORRELATIONS_TAB_ID } from '../../left/components/correlations_details'; +import { useDocumentDetailsContext } from '../../shared/context'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +jest.mock('@kbn/expandable-flyout'); +jest.mock('../../shared/context'); jest.mock('../../shared/hooks/use_fetch_related_alerts_by_session'); +const mockOpenLeftPanel = jest.fn(); +const eventId = 'eventId'; +const indexName = 'indexName'; const entityId = 'entityId'; const scopeId = 'scopeId'; -const ICON_TEST_ID = SUMMARY_ROW_ICON_TEST_ID(CORRELATIONS_RELATED_ALERTS_BY_SESSION_TEST_ID); -const VALUE_TEST_ID = SUMMARY_ROW_VALUE_TEST_ID(CORRELATIONS_RELATED_ALERTS_BY_SESSION_TEST_ID); +const TEXT_TEST_ID = SUMMARY_ROW_TEXT_TEST_ID(CORRELATIONS_RELATED_ALERTS_BY_SESSION_TEST_ID); +const BUTTON_TEST_ID = SUMMARY_ROW_BUTTON_TEST_ID(CORRELATIONS_RELATED_ALERTS_BY_SESSION_TEST_ID); const LOADING_TEST_ID = SUMMARY_ROW_LOADING_TEST_ID(CORRELATIONS_RELATED_ALERTS_BY_SESSION_TEST_ID); const renderRelatedAlertsBySession = () => @@ -34,34 +44,40 @@ const renderRelatedAlertsBySession = () => ); describe('', () => { - it('should render many related alerts correctly', () => { + beforeEach(() => { + jest.clearAllMocks(); + + (useDocumentDetailsContext as jest.Mock).mockReturnValue({ + eventId, + indexName, + scopeId, + isPreviewMode: false, + }); + (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ openLeftPanel: mockOpenLeftPanel }); + }); + + it('should render single related alerts correctly', () => { (useFetchRelatedAlertsBySession as jest.Mock).mockReturnValue({ loading: false, error: false, - dataCount: 2, + dataCount: 1, }); const { getByTestId } = renderRelatedAlertsBySession(); - expect(getByTestId(ICON_TEST_ID)).toBeInTheDocument(); - const value = getByTestId(VALUE_TEST_ID); - expect(value).toBeInTheDocument(); - expect(value).toHaveTextContent('2 alerts related by session'); - expect(getByTestId(VALUE_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(TEXT_TEST_ID)).toHaveTextContent('Alert related by session'); + expect(getByTestId(BUTTON_TEST_ID)).toHaveTextContent('1'); }); - it('should render single related alerts correctly', () => { + it('should render multiple related alerts correctly', () => { (useFetchRelatedAlertsBySession as jest.Mock).mockReturnValue({ loading: false, error: false, - dataCount: 1, + dataCount: 2, }); const { getByTestId } = renderRelatedAlertsBySession(); - expect(getByTestId(ICON_TEST_ID)).toBeInTheDocument(); - const value = getByTestId(VALUE_TEST_ID); - expect(value).toBeInTheDocument(); - expect(value).toHaveTextContent('1 alert related by session'); - expect(getByTestId(VALUE_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(TEXT_TEST_ID)).toHaveTextContent('Alerts related by session'); + expect(getByTestId(BUTTON_TEST_ID)).toHaveTextContent('2'); }); it('should render loading skeleton', () => { @@ -82,4 +98,28 @@ describe('', () => { const { container } = renderRelatedAlertsBySession(); expect(container).toBeEmptyDOMElement(); }); + + it('should open the expanded section to the correct tab when the number is clicked', () => { + (useFetchRelatedAlertsBySession as jest.Mock).mockReturnValue({ + loading: false, + error: false, + dataCount: 1, + }); + + const { getByTestId } = renderRelatedAlertsBySession(); + getByTestId(BUTTON_TEST_ID).click(); + + expect(mockOpenLeftPanel).toHaveBeenCalledWith({ + id: DocumentDetailsLeftPanelKey, + path: { + tab: LeftPanelInsightsTab, + subTab: CORRELATIONS_TAB_ID, + }, + params: { + id: eventId, + indexName, + scopeId, + }, + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_alerts_by_session.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_alerts_by_session.tsx index 4b41389137fad..9037ebca232a0 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_alerts_by_session.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_alerts_by_session.tsx @@ -5,14 +5,13 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; +import { CORRELATIONS_TAB_ID } from '../../left/components/correlations_details'; import { useFetchRelatedAlertsBySession } from '../../shared/hooks/use_fetch_related_alerts_by_session'; import { InsightsSummaryRow } from './insights_summary_row'; import { CORRELATIONS_RELATED_ALERTS_BY_SESSION_TEST_ID } from './test_ids'; -const ICON = 'warning'; - export interface RelatedAlertsBySessionProps { /** * Value of the process.entry_leader.entity_id field @@ -35,21 +34,25 @@ export const RelatedAlertsBySession: React.VFC = ({ entityId, scopeId, }); - const text = ( - + + const text = useMemo( + () => ( + + ), + [dataCount] ); return ( diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_cases.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_cases.test.tsx index 3d20e6399af38..e55d0e109d1d7 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_cases.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_cases.test.tsx @@ -10,19 +10,29 @@ import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { render } from '@testing-library/react'; import { CORRELATIONS_RELATED_CASES_TEST_ID, - SUMMARY_ROW_ICON_TEST_ID, + SUMMARY_ROW_TEXT_TEST_ID, SUMMARY_ROW_LOADING_TEST_ID, - SUMMARY_ROW_VALUE_TEST_ID, + SUMMARY_ROW_BUTTON_TEST_ID, } from './test_ids'; import { RelatedCases } from './related_cases'; import { useFetchRelatedCases } from '../../shared/hooks/use_fetch_related_cases'; +import { DocumentDetailsLeftPanelKey } from '../../shared/constants/panel_keys'; +import { LeftPanelInsightsTab } from '../../left'; +import { CORRELATIONS_TAB_ID } from '../../left/components/correlations_details'; +import { useDocumentDetailsContext } from '../../shared/context'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +jest.mock('@kbn/expandable-flyout'); +jest.mock('../../shared/context'); jest.mock('../../shared/hooks/use_fetch_related_cases'); +const mockOpenLeftPanel = jest.fn(); const eventId = 'eventId'; +const indexName = 'indexName'; +const scopeId = 'scopeId'; -const ICON_TEST_ID = SUMMARY_ROW_ICON_TEST_ID(CORRELATIONS_RELATED_CASES_TEST_ID); -const VALUE_TEST_ID = SUMMARY_ROW_VALUE_TEST_ID(CORRELATIONS_RELATED_CASES_TEST_ID); +const TEXT_TEST_ID = SUMMARY_ROW_TEXT_TEST_ID(CORRELATIONS_RELATED_CASES_TEST_ID); +const BUTTON_TEST_ID = SUMMARY_ROW_BUTTON_TEST_ID(CORRELATIONS_RELATED_CASES_TEST_ID); const LOADING_TEST_ID = SUMMARY_ROW_LOADING_TEST_ID(CORRELATIONS_RELATED_CASES_TEST_ID); const renderRelatedCases = () => @@ -33,34 +43,40 @@ const renderRelatedCases = () => ); describe('', () => { - it('should render many related cases correctly', () => { + beforeEach(() => { + jest.clearAllMocks(); + + (useDocumentDetailsContext as jest.Mock).mockReturnValue({ + eventId, + indexName, + scopeId, + isPreviewMode: false, + }); + (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ openLeftPanel: mockOpenLeftPanel }); + }); + + it('should render single related case correctly', () => { (useFetchRelatedCases as jest.Mock).mockReturnValue({ loading: false, error: false, - dataCount: 2, + dataCount: 1, }); const { getByTestId } = renderRelatedCases(); - expect(getByTestId(ICON_TEST_ID)).toBeInTheDocument(); - const value = getByTestId(VALUE_TEST_ID); - expect(value).toBeInTheDocument(); - expect(value).toHaveTextContent('2 related cases'); - expect(getByTestId(VALUE_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(TEXT_TEST_ID)).toHaveTextContent('Related case'); + expect(getByTestId(BUTTON_TEST_ID)).toHaveTextContent('1'); }); - it('should render single related case correctly', () => { + it('should render multiple related cases correctly', () => { (useFetchRelatedCases as jest.Mock).mockReturnValue({ loading: false, error: false, - dataCount: 1, + dataCount: 2, }); const { getByTestId } = renderRelatedCases(); - expect(getByTestId(ICON_TEST_ID)).toBeInTheDocument(); - const value = getByTestId(VALUE_TEST_ID); - expect(value).toBeInTheDocument(); - expect(value).toHaveTextContent('1 related case'); - expect(getByTestId(VALUE_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(TEXT_TEST_ID)).toHaveTextContent('Related cases'); + expect(getByTestId(BUTTON_TEST_ID)).toHaveTextContent('2'); }); it('should render loading skeleton', () => { @@ -81,4 +97,28 @@ describe('', () => { const { container } = renderRelatedCases(); expect(container).toBeEmptyDOMElement(); }); + + it('should open the expanded section to the correct tab when the number is clicked', () => { + (useFetchRelatedCases as jest.Mock).mockReturnValue({ + loading: false, + error: false, + dataCount: 1, + }); + + const { getByTestId } = renderRelatedCases(); + getByTestId(BUTTON_TEST_ID).click(); + + expect(mockOpenLeftPanel).toHaveBeenCalledWith({ + id: DocumentDetailsLeftPanelKey, + path: { + tab: LeftPanelInsightsTab, + subTab: CORRELATIONS_TAB_ID, + }, + params: { + id: eventId, + indexName, + scopeId, + }, + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_cases.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_cases.tsx index d45cc971dc046..8a01b21799d86 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_cases.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/related_cases.tsx @@ -5,13 +5,12 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useFetchRelatedCases } from '../../shared/hooks/use_fetch_related_cases'; -import { InsightsSummaryRow } from './insights_summary_row'; +import { CORRELATIONS_TAB_ID } from '../../left/components/correlations_details'; import { CORRELATIONS_RELATED_CASES_TEST_ID } from './test_ids'; - -const ICON = 'warning'; +import { InsightsSummaryRow } from './insights_summary_row'; +import { useFetchRelatedCases } from '../../shared/hooks/use_fetch_related_cases'; export interface RelatedCasesProps { /** @@ -21,25 +20,29 @@ export interface RelatedCasesProps { } /** - * + * Show related cases in summary row */ export const RelatedCases: React.VFC = ({ eventId }) => { const { loading, error, dataCount } = useFetchRelatedCases({ eventId }); - const text = ( - + + const text = useMemo( + () => ( + + ), + [dataCount] ); return ( diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/suppressed_alerts.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/suppressed_alerts.test.tsx index b5954c251c014..331283e194ed0 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/suppressed_alerts.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/suppressed_alerts.test.tsx @@ -9,16 +9,27 @@ import React from 'react'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { render } from '@testing-library/react'; import { - SUMMARY_ROW_ICON_TEST_ID, - SUMMARY_ROW_VALUE_TEST_ID, + SUMMARY_ROW_TEXT_TEST_ID, CORRELATIONS_SUPPRESSED_ALERTS_TEST_ID, CORRELATIONS_SUPPRESSED_ALERTS_TECHNICAL_PREVIEW_TEST_ID, + SUMMARY_ROW_BUTTON_TEST_ID, } from './test_ids'; import { SuppressedAlerts } from './suppressed_alerts'; import { isSuppressionRuleInGA } from '../../../../../common/detection_engine/utils'; +import { DocumentDetailsLeftPanelKey } from '../../shared/constants/panel_keys'; +import { LeftPanelInsightsTab } from '../../left'; +import { CORRELATIONS_TAB_ID } from '../../left/components/correlations_details'; +import { useDocumentDetailsContext } from '../../shared/context'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; -const ICON_TEST_ID = SUMMARY_ROW_ICON_TEST_ID(CORRELATIONS_SUPPRESSED_ALERTS_TEST_ID); -const VALUE_TEST_ID = SUMMARY_ROW_VALUE_TEST_ID(CORRELATIONS_SUPPRESSED_ALERTS_TEST_ID); +jest.mock('@kbn/expandable-flyout'); +jest.mock('../../shared/context'); +jest.mock('../../../../../common/detection_engine/utils', () => ({ + isSuppressionRuleInGA: jest.fn().mockReturnValue(false), +})); + +const TEXT_TEST_ID = SUMMARY_ROW_TEXT_TEST_ID(CORRELATIONS_SUPPRESSED_ALERTS_TEST_ID); +const BUTTON_TEST_ID = SUMMARY_ROW_BUTTON_TEST_ID(CORRELATIONS_SUPPRESSED_ALERTS_TEST_ID); const renderSuppressedAlerts = (alertSuppressionCount: number) => render( @@ -27,34 +38,30 @@ const renderSuppressedAlerts = (alertSuppressionCount: number) => ); -jest.mock('../../../../../common/detection_engine/utils', () => ({ - isSuppressionRuleInGA: jest.fn().mockReturnValue(false), -})); - +const mockOpenLeftPanel = jest.fn(); +const scopeId = 'scopeId'; +const eventId = 'eventId'; +const indexName = 'indexName'; const isSuppressionRuleInGAMock = isSuppressionRuleInGA as jest.Mock; describe('', () => { - it('should render zero suppressed alert correctly', () => { - const { getByTestId } = renderSuppressedAlerts(0); + beforeEach(() => { + jest.clearAllMocks(); - expect(getByTestId(ICON_TEST_ID)).toBeInTheDocument(); - const value = getByTestId(VALUE_TEST_ID); - expect(value).toBeInTheDocument(); - expect(value).toHaveTextContent('0 suppressed alert'); - expect(getByTestId(VALUE_TEST_ID)).toBeInTheDocument(); - expect( - getByTestId(CORRELATIONS_SUPPRESSED_ALERTS_TECHNICAL_PREVIEW_TEST_ID) - ).toBeInTheDocument(); + (useDocumentDetailsContext as jest.Mock).mockReturnValue({ + eventId, + indexName, + scopeId, + isPreviewMode: false, + }); + (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ openLeftPanel: mockOpenLeftPanel }); }); it('should render single suppressed alert correctly', () => { const { getByTestId } = renderSuppressedAlerts(1); - expect(getByTestId(ICON_TEST_ID)).toBeInTheDocument(); - const value = getByTestId(VALUE_TEST_ID); - expect(value).toBeInTheDocument(); - expect(value).toHaveTextContent('1 suppressed alert'); - expect(getByTestId(VALUE_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(TEXT_TEST_ID)).toHaveTextContent('Suppressed alert'); + expect(getByTestId(BUTTON_TEST_ID)).toHaveTextContent('1'); expect( getByTestId(CORRELATIONS_SUPPRESSED_ALERTS_TECHNICAL_PREVIEW_TEST_ID) ).toBeInTheDocument(); @@ -63,14 +70,8 @@ describe('', () => { it('should render multiple suppressed alerts row correctly', () => { const { getByTestId } = renderSuppressedAlerts(2); - expect(getByTestId(ICON_TEST_ID)).toBeInTheDocument(); - const value = getByTestId(VALUE_TEST_ID); - expect(value).toBeInTheDocument(); - expect(value).toHaveTextContent('2 suppressed alerts'); - expect(getByTestId(VALUE_TEST_ID)).toBeInTheDocument(); - expect( - getByTestId(CORRELATIONS_SUPPRESSED_ALERTS_TECHNICAL_PREVIEW_TEST_ID) - ).toBeInTheDocument(); + expect(getByTestId(TEXT_TEST_ID)).toHaveTextContent('Suppressed alerts'); + expect(getByTestId(BUTTON_TEST_ID)).toHaveTextContent('2'); }); it('should not render Technical Preview badge if rule type is in GA', () => { @@ -81,4 +82,22 @@ describe('', () => { queryByTestId(CORRELATIONS_SUPPRESSED_ALERTS_TECHNICAL_PREVIEW_TEST_ID) ).not.toBeInTheDocument(); }); + + it('should open the expanded section to the correct tab when the number is clicked', () => { + const { getByTestId } = renderSuppressedAlerts(1); + getByTestId(BUTTON_TEST_ID).click(); + + expect(mockOpenLeftPanel).toHaveBeenCalledWith({ + id: DocumentDetailsLeftPanelKey, + path: { + tab: LeftPanelInsightsTab, + subTab: CORRELATIONS_TAB_ID, + }, + params: { + id: eventId, + indexName, + scopeId, + }, + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/suppressed_alerts.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/suppressed_alerts.tsx index a8cd147a4ac14..c7eb50aeb383a 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/suppressed_alerts.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/suppressed_alerts.tsx @@ -5,18 +5,19 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiBetaBadge } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; import { i18n } from '@kbn/i18n'; -import { isSuppressionRuleInGA } from '../../../../../common/detection_engine/utils'; +import { CORRELATIONS_TAB_ID } from '../../left/components/correlations_details'; +import { InsightsSummaryRow } from './insights_summary_row'; import { CORRELATIONS_SUPPRESSED_ALERTS_TEST_ID, CORRELATIONS_SUPPRESSED_ALERTS_TECHNICAL_PREVIEW_TEST_ID, } from './test_ids'; -import { InsightsSummaryRow } from './insights_summary_row'; +import { isSuppressionRuleInGA } from '../../../../../common/detection_engine/utils'; const SUPPRESSED_ALERTS_COUNT_TECHNICAL_PREVIEW = i18n.translate( 'xpack.securitySolution.flyout.right.overview.insights.suppressedAlertsCountTechnicalPreview', @@ -43,21 +44,24 @@ export const SuppressedAlerts: React.VFC = ({ alertSuppressionCount, ruleType, }) => { + const text = useMemo( + () => ( + + ), + [alertSuppressionCount] + ); + return ( - } + expandedSubTab={CORRELATIONS_TAB_ID} data-test-subj={CORRELATIONS_SUPPRESSED_ALERTS_TEST_ID} key={`correlation-row-suppressed-alerts`} /> diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/test_ids.ts index e649c578bf487..959f8f106bb08 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/test_ids.ts @@ -104,8 +104,9 @@ export const INSIGHTS_CONTENT_TEST_ID = INSIGHTS_TEST_ID + CONTENT_TEST_ID; /* Summary row */ export const SUMMARY_ROW_LOADING_TEST_ID = (dataTestSubj: string) => `${dataTestSubj}Loading`; -export const SUMMARY_ROW_ICON_TEST_ID = (dataTestSubj: string) => `${dataTestSubj}Icon`; +export const SUMMARY_ROW_TEXT_TEST_ID = (dataTestSubj: string) => `${dataTestSubj}Text`; export const SUMMARY_ROW_VALUE_TEST_ID = (dataTestSubj: string) => `${dataTestSubj}Value`; +export const SUMMARY_ROW_BUTTON_TEST_ID = (dataTestSubj: string) => `${dataTestSubj}Button`; /* Entities */ @@ -146,6 +147,10 @@ export const ENTITIES_HOST_OVERVIEW_VULNERABILITIES_TEST_ID = /* Threat intelligence */ export const INSIGHTS_THREAT_INTELLIGENCE_TEST_ID = `${PREFIX}InsightsThreatIntelligence` as const; +export const INSIGHTS_THREAT_INTELLIGENCE_THREAT_MATCHES_TEST_ID = + `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}ThreatMatches` as const; +export const INSIGHTS_THREAT_INTELLIGENCE_ENRICHED_WITH_THREAT_INTELLIGENCE_TEST_ID = + `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}EnrichedWithThreatIntelligence` as const; /* Correlations */ diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/threat_intelligence_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/threat_intelligence_overview.test.tsx index af92283b781b5..f451862e0990e 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/threat_intelligence_overview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/threat_intelligence_overview.test.tsx @@ -6,18 +6,23 @@ */ import React from 'react'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { render } from '@testing-library/react'; -import { useExpandableFlyoutApi, type ExpandableFlyoutApi } from '@kbn/expandable-flyout'; -import { DocumentDetailsContext } from '../../shared/context'; -import { TestProviders } from '../../../../common/mock'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { useDocumentDetailsContext } from '../../shared/context'; import { ThreatIntelligenceOverview } from './threat_intelligence_overview'; import { DocumentDetailsLeftPanelKey } from '../../shared/constants/panel_keys'; import { LeftPanelInsightsTab } from '../../left'; import { useFetchThreatIntelligence } from '../hooks/use_fetch_threat_intelligence'; import { THREAT_INTELLIGENCE_TAB_ID } from '../../left/components/threat_intelligence_details'; -import { INSIGHTS_THREAT_INTELLIGENCE_TEST_ID } from './test_ids'; import { - EXPANDABLE_PANEL_CONTENT_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_ENRICHED_WITH_THREAT_INTELLIGENCE_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_THREAT_MATCHES_TEST_ID, + SUMMARY_ROW_BUTTON_TEST_ID, + SUMMARY_ROW_TEXT_TEST_ID, +} from './test_ids'; +import { EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID, EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID, EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID, @@ -25,6 +30,8 @@ import { EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID, } from '@kbn/security-solution-common'; +jest.mock('@kbn/expandable-flyout'); +jest.mock('../../shared/context'); jest.mock('../hooks/use_fetch_threat_intelligence'); const TOGGLE_ICON_TEST_ID = EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID( @@ -39,32 +46,45 @@ const TITLE_ICON_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID( const TITLE_TEXT_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID( INSIGHTS_THREAT_INTELLIGENCE_TEST_ID ); -const CONTENT_TEST_ID = EXPANDABLE_PANEL_CONTENT_TEST_ID(INSIGHTS_THREAT_INTELLIGENCE_TEST_ID); const LOADING_TEST_ID = EXPANDABLE_PANEL_LOADING_TEST_ID(INSIGHTS_THREAT_INTELLIGENCE_TEST_ID); +const THREAT_MATCHES_TEXT_TEST_ID = SUMMARY_ROW_TEXT_TEST_ID( + INSIGHTS_THREAT_INTELLIGENCE_THREAT_MATCHES_TEST_ID +); +const THREAT_MATCHES_BUTTON_TEST_ID = SUMMARY_ROW_BUTTON_TEST_ID( + INSIGHTS_THREAT_INTELLIGENCE_THREAT_MATCHES_TEST_ID +); +const ENRICHED_WITH_THREAT_INTELLIGENCE_TEXT_TEST_ID = SUMMARY_ROW_TEXT_TEST_ID( + INSIGHTS_THREAT_INTELLIGENCE_ENRICHED_WITH_THREAT_INTELLIGENCE_TEST_ID +); +const ENRICHED_WITH_THREAT_INTELLIGENCE_BUTTON_TEST_ID = SUMMARY_ROW_BUTTON_TEST_ID( + INSIGHTS_THREAT_INTELLIGENCE_ENRICHED_WITH_THREAT_INTELLIGENCE_TEST_ID +); -const panelContextValue = { - eventId: 'event id', - indexName: 'indexName', - dataFormattedForFieldBrowser: [], -} as unknown as DocumentDetailsContext; +const mockOpenLeftPanel = jest.fn(); +const eventId = 'eventId'; +const indexName = 'indexName'; +const scopeId = 'scopeId'; +const dataFormattedForFieldBrowser = ['scopeId']; -jest.mock('@kbn/expandable-flyout'); - -const renderThreatIntelligenceOverview = (contextValue: DocumentDetailsContext) => ( - - +const renderThreatIntelligenceOverview = () => + render( + - - -); - -const flyoutContextValue = { - openLeftPanel: jest.fn(), -} as unknown as ExpandableFlyoutApi; + + ); describe('', () => { - beforeAll(() => { - jest.mocked(useExpandableFlyoutApi).mockReturnValue(flyoutContextValue); + beforeEach(() => { + jest.clearAllMocks(); + + (useDocumentDetailsContext as jest.Mock).mockReturnValue({ + eventId, + indexName, + scopeId, + dataFormattedForFieldBrowser, + isPreviewMode: false, + }); + (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ openLeftPanel: mockOpenLeftPanel }); }); it('should render wrapper component', () => { @@ -72,9 +92,7 @@ describe('', () => { loading: false, }); - const { getByTestId, queryByTestId } = render( - renderThreatIntelligenceOverview(panelContextValue) - ); + const { getByTestId, queryByTestId } = renderThreatIntelligenceOverview(); expect(queryByTestId(TOGGLE_ICON_TEST_ID)).not.toBeInTheDocument(); expect(getByTestId(TITLE_ICON_TEST_ID)).toBeInTheDocument(); @@ -82,14 +100,19 @@ describe('', () => { expect(queryByTestId(TITLE_TEXT_TEST_ID)).not.toBeInTheDocument(); }); - it('should not render link if isPrenviewMode is true', () => { + it('should not render link if isPreviewMode is true', () => { + (useDocumentDetailsContext as jest.Mock).mockReturnValue({ + eventId, + indexName, + scopeId, + dataFormattedForFieldBrowser, + isPreviewMode: true, + }); (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ loading: false, }); - const { getByTestId, queryByTestId } = render( - renderThreatIntelligenceOverview({ ...panelContextValue, isPreviewMode: true }) - ); + const { getByTestId, queryByTestId } = renderThreatIntelligenceOverview(); expect(queryByTestId(TOGGLE_ICON_TEST_ID)).not.toBeInTheDocument(); expect(queryByTestId(TITLE_ICON_TEST_ID)).not.toBeInTheDocument(); @@ -104,13 +127,15 @@ describe('', () => { threatEnrichmentsCount: 1, }); - const { getByTestId } = render(renderThreatIntelligenceOverview(panelContextValue)); + const { getByTestId } = renderThreatIntelligenceOverview(); expect(getByTestId(TITLE_LINK_TEST_ID)).toHaveTextContent('Threat intelligence'); - expect(getByTestId(CONTENT_TEST_ID)).toHaveTextContent('1 threat match detected'); - expect(getByTestId(CONTENT_TEST_ID)).toHaveTextContent( - '1 field enriched with threat intelligence' + expect(getByTestId(THREAT_MATCHES_TEXT_TEST_ID)).toHaveTextContent('Threat match detected'); + expect(getByTestId(THREAT_MATCHES_BUTTON_TEST_ID)).toHaveTextContent('1'); + expect(getByTestId(ENRICHED_WITH_THREAT_INTELLIGENCE_TEXT_TEST_ID)).toHaveTextContent( + 'Field enriched with threat intelligence' ); + expect(getByTestId(ENRICHED_WITH_THREAT_INTELLIGENCE_BUTTON_TEST_ID)).toHaveTextContent('1'); }); it('should render 2 matches detected and 2 fields enriched', () => { @@ -120,72 +145,85 @@ describe('', () => { threatEnrichmentsCount: 2, }); - const { getByTestId } = render(renderThreatIntelligenceOverview(panelContextValue)); + const { getByTestId } = renderThreatIntelligenceOverview(); expect(getByTestId(TITLE_LINK_TEST_ID)).toHaveTextContent('Threat intelligence'); - expect(getByTestId(CONTENT_TEST_ID)).toHaveTextContent('2 threat matches detected'); - expect(getByTestId(CONTENT_TEST_ID)).toHaveTextContent( - '2 fields enriched with threat intelligence' + expect(getByTestId(THREAT_MATCHES_TEXT_TEST_ID)).toHaveTextContent('Threat matches detected'); + expect(getByTestId(THREAT_MATCHES_BUTTON_TEST_ID)).toHaveTextContent('2'); + expect(getByTestId(ENRICHED_WITH_THREAT_INTELLIGENCE_TEXT_TEST_ID)).toHaveTextContent( + 'Fields enriched with threat intelligence' ); + expect(getByTestId(ENRICHED_WITH_THREAT_INTELLIGENCE_BUTTON_TEST_ID)).toHaveTextContent('2'); }); - it('should render 0 fields enriched', () => { + it('should render loading', () => { (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ - loading: false, - threatMatchesCount: 1, - threatEnrichmentsCount: 0, + loading: true, }); - const { getByTestId } = render(renderThreatIntelligenceOverview(panelContextValue)); + const { getByTestId } = renderThreatIntelligenceOverview(); - expect(getByTestId(CONTENT_TEST_ID)).toHaveTextContent( - '0 fields enriched with threat intelligence' - ); + expect(getByTestId(LOADING_TEST_ID)).toBeInTheDocument(); }); - it('should render 0 matches detected', () => { + it('should navigate to left section Insights tab when clicking on button', () => { (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ loading: false, - threatMatchesCount: 0, - threatEnrichmentsCount: 2, + threatMatchesCount: 1, + threatEnrichmentsCount: 1, }); + const { getByTestId } = renderThreatIntelligenceOverview(); - const { getByTestId } = render(renderThreatIntelligenceOverview(panelContextValue)); - - expect(getByTestId(CONTENT_TEST_ID)).toHaveTextContent('0 threat matches detected'); - }); - - it('should render loading', () => { - (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ - loading: true, + getByTestId(TITLE_LINK_TEST_ID).click(); + expect(mockOpenLeftPanel).toHaveBeenCalledWith({ + id: DocumentDetailsLeftPanelKey, + path: { + tab: LeftPanelInsightsTab, + subTab: THREAT_INTELLIGENCE_TAB_ID, + }, + params: { + id: eventId, + indexName, + scopeId, + }, }); - - const { getByTestId } = render(renderThreatIntelligenceOverview(panelContextValue)); - - expect(getByTestId(LOADING_TEST_ID)).toBeInTheDocument(); }); - it('should navigate to left section Insights tab when clicking on button', () => { + it('should open the expanded section to the correct tab when the number is clicked', () => { (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ loading: false, threatMatchesCount: 1, threatEnrichmentsCount: 1, }); - const { getByTestId } = render( - - - - - - ); - getByTestId(TITLE_LINK_TEST_ID).click(); - expect(flyoutContextValue.openLeftPanel).toHaveBeenCalledWith({ + const { getByTestId } = renderThreatIntelligenceOverview(); + getByTestId(THREAT_MATCHES_BUTTON_TEST_ID).click(); + + expect(mockOpenLeftPanel).toHaveBeenCalledWith({ + id: DocumentDetailsLeftPanelKey, + path: { + tab: LeftPanelInsightsTab, + subTab: THREAT_INTELLIGENCE_TAB_ID, + }, + params: { + id: eventId, + indexName, + scopeId, + }, + }); + + getByTestId(ENRICHED_WITH_THREAT_INTELLIGENCE_BUTTON_TEST_ID).click(); + + expect(mockOpenLeftPanel).toHaveBeenCalledWith({ id: DocumentDetailsLeftPanelKey, - path: { tab: LeftPanelInsightsTab, subTab: THREAT_INTELLIGENCE_TAB_ID }, + path: { + tab: LeftPanelInsightsTab, + subTab: THREAT_INTELLIGENCE_TAB_ID, + }, params: { - id: panelContextValue.eventId, - indexName: panelContextValue.indexName, + id: eventId, + indexName, + scopeId, }, }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/threat_intelligence_overview.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/threat_intelligence_overview.tsx index 10b23ecfc2340..0a737a973ea2d 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/threat_intelligence_overview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/threat_intelligence_overview.tsx @@ -14,11 +14,28 @@ import { ExpandablePanel } from '@kbn/security-solution-common'; import { useFetchThreatIntelligence } from '../hooks/use_fetch_threat_intelligence'; import { InsightsSummaryRow } from './insights_summary_row'; import { useDocumentDetailsContext } from '../../shared/context'; -import { INSIGHTS_THREAT_INTELLIGENCE_TEST_ID } from './test_ids'; +import { + INSIGHTS_THREAT_INTELLIGENCE_ENRICHED_WITH_THREAT_INTELLIGENCE_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_THREAT_MATCHES_TEST_ID, +} from './test_ids'; import { DocumentDetailsLeftPanelKey } from '../../shared/constants/panel_keys'; import { LeftPanelInsightsTab } from '../../left'; import { THREAT_INTELLIGENCE_TAB_ID } from '../../left/components/threat_intelligence_details'; +const TITLE = ( + +); +const TOOLTIP = ( + +); + /** * Threat intelligence section under Insights section, overview tab. * The component fetches the necessary data, then pass it down to the InsightsSubSection component for loading and error state, @@ -53,26 +70,38 @@ export const ThreatIntelligenceOverview: FC = () => { !isPreviewMode ? { callback: goToThreatIntelligenceTab, - tooltip: ( - - ), + tooltip: TOOLTIP, } : undefined, [isPreviewMode, goToThreatIntelligenceTab] ); + const threatMatchCountText = useMemo( + () => ( + + ), + [threatMatchesCount] + ); + + const threatEnrichmentsCountText = useMemo( + () => ( + + ), + [threatEnrichmentsCount] + ); + return ( - ), + title: TITLE, link, iconType: !isPreviewMode ? 'arrowStart' : undefined, }} @@ -81,32 +110,20 @@ export const ThreatIntelligenceOverview: FC = () => { > - } - data-test-subj={INSIGHTS_THREAT_INTELLIGENCE_TEST_ID} + expandedSubTab={THREAT_INTELLIGENCE_TAB_ID} + data-test-subj={INSIGHTS_THREAT_INTELLIGENCE_THREAT_MATCHES_TEST_ID} /> - } - data-test-subj={INSIGHTS_THREAT_INTELLIGENCE_TEST_ID} + expandedSubTab={THREAT_INTELLIGENCE_TAB_ID} + data-test-subj={INSIGHTS_THREAT_INTELLIGENCE_ENRICHED_WITH_THREAT_INTELLIGENCE_TEST_ID} /> diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 2ae87164a3dec..a3ea9f7c4c7d7 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -38783,12 +38783,10 @@ "xpack.securitySolution.flyout.right.insights.entities.noDataDescription": "Les informations de l'hôte et de l'utilisateur ne sont pas disponibles pour cette alerte.", "xpack.securitySolution.flyout.right.insights.entities.suppressedAlertTechnicalPreviewTooltip": "Cette fonctionnalité est en version d'évaluation technique et pourra être modifiée ou retirée complètement dans une future version. Elastic s'efforcera de corriger tout problème, mais les fonctionnalités des versions d'évaluation technique ne sont pas soumises aux SLA de support des fonctionnalités officielles en disponibilité générale.", "xpack.securitySolution.flyout.right.insights.entities.userLoadingAriaLabel": "aperçu de l'utilisateur", - "xpack.securitySolution.flyout.right.insights.insightSummaryButtonIconAriaLabel": "Icône de ligne de résumé d'informations", "xpack.securitySolution.flyout.right.insights.insightSummaryLoadingAriaLabel": "Chargement des informations pour {value}", "xpack.securitySolution.flyout.right.insights.prevalence.noDataDescription": "Aucune donnée de prévalence disponible.", "xpack.securitySolution.flyout.right.insights.prevalence.prevalenceTitle": "Prévalence", "xpack.securitySolution.flyout.right.insights.prevalence.prevalenceTooltip": "Afficher toutes les prévalences", - "xpack.securitySolution.flyout.right.insights.prevalence.rowDescription": "{field}, {value} est inhabituel", "xpack.securitySolution.flyout.right.insights.sectionTitle": "Informations exploitables", "xpack.securitySolution.flyout.right.insights.threatIntelligence.threatEnrichmentDescription": "{count, plural, one {champ enrichi} other {champs enrichis}} avec Threat Intelligence", "xpack.securitySolution.flyout.right.insights.threatIntelligence.threatIntelligenceTitle": "Threat Intelligence", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b1a8859d20830..cb57653916ec0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -38526,12 +38526,10 @@ "xpack.securitySolution.flyout.right.insights.entities.noDataDescription": "このアラートでは、ホストとユーザーの情報は利用できません。", "xpack.securitySolution.flyout.right.insights.entities.suppressedAlertTechnicalPreviewTooltip": "この機能はテクニカルプレビュー中であり、将来のリリースでは変更されたり完全に削除されたりする場合があります。Elasticはすべての問題の修正に努めますが、テクニカルプレビュー中の機能には正式なGA機能のサポートSLAが適用されません。", "xpack.securitySolution.flyout.right.insights.entities.userLoadingAriaLabel": "ユーザー概要", - "xpack.securitySolution.flyout.right.insights.insightSummaryButtonIconAriaLabel": "インサイト概要行アイコン", "xpack.securitySolution.flyout.right.insights.insightSummaryLoadingAriaLabel": "{value}のインサイトを読み込み中", "xpack.securitySolution.flyout.right.insights.prevalence.noDataDescription": "発生率データはありません。", "xpack.securitySolution.flyout.right.insights.prevalence.prevalenceTitle": "発生率", "xpack.securitySolution.flyout.right.insights.prevalence.prevalenceTooltip": "すべての発生率を表示", - "xpack.securitySolution.flyout.right.insights.prevalence.rowDescription": "{field}、{value}は共通していません", "xpack.securitySolution.flyout.right.insights.sectionTitle": "インサイト", "xpack.securitySolution.flyout.right.insights.threatIntelligence.threatEnrichmentDescription": "{count, plural, other {フィールド}}はヒートインテリジェンスでエンリッチされています", "xpack.securitySolution.flyout.right.insights.threatIntelligence.threatIntelligenceTitle": "脅威インテリジェンス", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index b040c7ab587df..6fe8f1a8e67cd 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -38571,12 +38571,10 @@ "xpack.securitySolution.flyout.right.insights.entities.noDataDescription": "主机和用户信息对此告警不可用。", "xpack.securitySolution.flyout.right.insights.entities.suppressedAlertTechnicalPreviewTooltip": "此功能处于技术预览状态,在未来版本中可能会更改或完全移除。Elastic 将努力修复任何问题,但处于技术预览状态的功能不受正式 GA 功能支持 SLA 的约束。", "xpack.securitySolution.flyout.right.insights.entities.userLoadingAriaLabel": "用户概览", - "xpack.securitySolution.flyout.right.insights.insightSummaryButtonIconAriaLabel": "洞见摘要行图标", "xpack.securitySolution.flyout.right.insights.insightSummaryLoadingAriaLabel": "正在加载 {value} 的洞见", "xpack.securitySolution.flyout.right.insights.prevalence.noDataDescription": "无普及性数据可用。", "xpack.securitySolution.flyout.right.insights.prevalence.prevalenceTitle": "普及率", "xpack.securitySolution.flyout.right.insights.prevalence.prevalenceTooltip": "显示所有普及率", - "xpack.securitySolution.flyout.right.insights.prevalence.rowDescription": "{field},{value} 不常见", "xpack.securitySolution.flyout.right.insights.sectionTitle": "洞见", "xpack.securitySolution.flyout.right.insights.threatIntelligence.threatEnrichmentDescription": "{count, plural, other {个字段}}已使用威胁情报扩充", "xpack.securitySolution.flyout.right.insights.threatIntelligence.threatIntelligenceTitle": "威胁情报", diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts index 6b451801a58c5..87dd30b293f0d 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts @@ -27,7 +27,6 @@ import { DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_CONTENT, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_HEADER, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_HEADER, - DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VALUES, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INVESTIGATION_GUIDE_BUTTON, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INVESTIGATION_SECTION_HEADER, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_MITRE_ATTACK_DETAILS, @@ -43,6 +42,8 @@ import { DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HOST_OVERVIEW_LINK, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_USER_OVERVIEW_LINK, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW_NO_DATA, + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_THREAT_MATCHES_VALUE, + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_ENRICHED_WITH_THREAT_INTELLIGENCE_VALUE, } from '../../../../screens/expandable_flyout/alert_details_right_panel_overview_tab'; import { navigateToCorrelationsDetails, @@ -348,14 +349,12 @@ describe( 'Threat intelligence' ); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VALUES) - .eq(0) - .should('have.text', '0 threat matches detected'); // TODO work on getting proper IoC data to get proper data here - - // field with threat enrichement - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VALUES) - .eq(1) - .should('have.text', '0 fields enriched with threat intelligence'); // TODO work on getting proper IoC data to get proper data here + cy.get( + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_THREAT_MATCHES_VALUE + ).should('have.text', '0'); // TODO work on getting proper IoC data to get proper data here + cy.get( + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_ENRICHED_WITH_THREAT_INTELLIGENCE_VALUE + ).should('have.text', '0'); // TODO work on getting proper IoC data to get proper data here cy.log('should navigate to left panel Threat Intelligence tab'); @@ -385,19 +384,19 @@ describe( ); // cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES_SUPPRESSED_ALERTS) // .should('be.visible') - // .and('have.text', '1 suppressed alert'); // TODO populate rule with alert suppression + // .and('have.text', '1'); // TODO populate rule with alert suppression cy.get( DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES_RELATED_ALERTS_BY_ANCESTRY - ).should('have.text', '1 alert related by ancestry'); + ).should('have.text', '1'); cy.get( DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES_RELATED_ALERTS_BY_SAME_SOURCE_EVENT - ).should('have.text', '1 alert related by source event'); + ).should('have.text', '1'); cy.get( DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES_RELATED_ALERTS_BY_SESSION - ).should('have.text', '1 alert related by session'); + ).should('have.text', '1'); cy.get( DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES_RELATED_CASES - ).should('have.text', '1 related case'); + ).should('have.text', '1'); cy.log('should navigate to left panel Correlations tab'); 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 676dec4aaeab1..2d267bee721dc 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 @@ -77,25 +77,29 @@ export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_USER_OVERVIEW_LINK = getDataTe export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_HEADER = getDataTestSubjectSelector('securitySolutionFlyoutInsightsThreatIntelligenceTitleLink'); -export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VALUES = - getDataTestSubjectSelector('securitySolutionFlyoutInsightsThreatIntelligenceValue'); +export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_THREAT_MATCHES_VALUE = + getDataTestSubjectSelector('securitySolutionFlyoutInsightsThreatIntelligenceThreatMatchesButton'); +export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_ENRICHED_WITH_THREAT_INTELLIGENCE_VALUE = + getDataTestSubjectSelector( + 'securitySolutionFlyoutInsightsThreatIntelligenceEnrichedWithThreatIntelligenceButton' + ); /* Insights Correlations */ export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_HEADER = getDataTestSubjectSelector('securitySolutionFlyoutCorrelationsTitleLink'); export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES_SUPPRESSED_ALERTS = - getDataTestSubjectSelector('securitySolutionFlyoutCorrelationsSuppressedAlertsValue'); + getDataTestSubjectSelector('securitySolutionFlyoutCorrelationsSuppressedAlertsButton'); export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES_RELATED_ALERTS_BY_ANCESTRY = - getDataTestSubjectSelector('securitySolutionFlyoutCorrelationsRelatedAlertsByAncestryValue'); + getDataTestSubjectSelector('securitySolutionFlyoutCorrelationsRelatedAlertsByAncestryButton'); export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES_RELATED_ALERTS_BY_SAME_SOURCE_EVENT = getDataTestSubjectSelector( - 'securitySolutionFlyoutCorrelationsRelatedAlertsBySameSourceEventValue' + 'securitySolutionFlyoutCorrelationsRelatedAlertsBySameSourceEventButton' ); export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES_RELATED_ALERTS_BY_SESSION = - getDataTestSubjectSelector('securitySolutionFlyoutCorrelationsRelatedAlertsBySessionValue'); + getDataTestSubjectSelector('securitySolutionFlyoutCorrelationsRelatedAlertsBySessionButton'); export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES_RELATED_CASES = - getDataTestSubjectSelector('securitySolutionFlyoutCorrelationsRelatedCasesValue'); + getDataTestSubjectSelector('securitySolutionFlyoutCorrelationsRelatedCasesButton'); /* Insights Prevalence */