diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.test.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.test.tsx index e1512b8b7ada..5c7a900765fe 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.test.tsx @@ -11,11 +11,18 @@ import { LeftPanelContext } from '../context'; import { PrevalenceDetails } from './prevalence_details'; import { PREVALENCE_DETAILS_LOADING_TEST_ID, + PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID, + PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID, PREVALENCE_DETAILS_TABLE_ERROR_TEST_ID, + PREVALENCE_DETAILS_TABLE_FIELD_CELL_TEST_ID, + PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID, PREVALENCE_DETAILS_TABLE_TEST_ID, + PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID, + PREVALENCE_DETAILS_TABLE_VALUE_CELL_TEST_ID, } from './test_ids'; import { usePrevalence } from '../../shared/hooks/use_prevalence'; import { TestProviders } from '../../../common/mock'; +import { licenseService } from '../../../common/hooks/use_license'; jest.mock('../../shared/hooks/use_prevalence'); @@ -27,6 +34,17 @@ jest.mock('react-redux', () => { useDispatch: () => mockDispatch, }; }); +jest.mock('../../../common/hooks/use_license', () => { + const licenseServiceInstance = { + isPlatinumPlus: jest.fn(), + }; + return { + licenseService: licenseServiceInstance, + useLicense: () => { + return licenseServiceInstance; + }, + }; +}); const panelContextValue = { eventId: 'event id', @@ -36,7 +54,13 @@ const panelContextValue = { } as unknown as LeftPanelContext; describe('PrevalenceDetails', () => { - it('should render the table', () => { + const licenseServiceMock = licenseService as jest.Mocked; + + beforeEach(() => { + licenseServiceMock.isPlatinumPlus.mockReturnValue(true); + }); + + it('should render the table with all columns if license is platinum', () => { const field1 = 'field1'; const field2 = 'field2'; (usePrevalence as jest.Mock).mockReturnValue({ @@ -62,7 +86,7 @@ describe('PrevalenceDetails', () => { ], }); - const { getByTestId } = render( + const { getByTestId, getAllByTestId, queryByTestId } = render( @@ -71,6 +95,74 @@ describe('PrevalenceDetails', () => { ); expect(getByTestId(PREVALENCE_DETAILS_TABLE_TEST_ID)).toBeInTheDocument(); + expect(getAllByTestId(PREVALENCE_DETAILS_TABLE_FIELD_CELL_TEST_ID).length).toBeGreaterThan(1); + expect(getAllByTestId(PREVALENCE_DETAILS_TABLE_VALUE_CELL_TEST_ID).length).toBeGreaterThan(1); + expect( + getAllByTestId(PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID).length + ).toBeGreaterThan(1); + expect(getAllByTestId(PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID).length).toBeGreaterThan( + 1 + ); + expect( + getAllByTestId(PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID).length + ).toBeGreaterThan(1); + expect( + getAllByTestId(PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID).length + ).toBeGreaterThan(1); + expect(queryByTestId(`${PREVALENCE_DETAILS_TABLE_TEST_ID}UpSell`)).not.toBeInTheDocument(); + }); + + it('should render the table with only basic columns if license is not platinum', () => { + const field1 = 'field1'; + const field2 = 'field2'; + (usePrevalence as jest.Mock).mockReturnValue({ + loading: false, + error: false, + data: [ + { + field: field1, + value: 'value1', + alertCount: 1, + docCount: 1, + hostPrevalence: 0.05, + userPrevalence: 0.1, + }, + { + field: field2, + value: 'value2', + alertCount: 1, + docCount: 1, + hostPrevalence: 0.5, + userPrevalence: 0.05, + }, + ], + }); + licenseServiceMock.isPlatinumPlus.mockReturnValue(false); + + const { getByTestId, getAllByTestId } = render( + + + + + + ); + + expect(getByTestId(PREVALENCE_DETAILS_TABLE_TEST_ID)).toBeInTheDocument(); + expect(getAllByTestId(PREVALENCE_DETAILS_TABLE_FIELD_CELL_TEST_ID).length).toBeGreaterThan(1); + expect(getAllByTestId(PREVALENCE_DETAILS_TABLE_VALUE_CELL_TEST_ID).length).toBeGreaterThan(1); + expect( + getAllByTestId(PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID).length + ).toBeGreaterThan(1); + expect(getAllByTestId(PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID).length).toBeGreaterThan( + 1 + ); + expect( + getAllByTestId(PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID).length + ).toBeGreaterThan(1); + expect( + getAllByTestId(PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID).length + ).toBeGreaterThan(1); + expect(getByTestId(`${PREVALENCE_DETAILS_TABLE_TEST_ID}UpSell`)).toBeInTheDocument(); }); it('should render loading', () => { diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.tsx index b11622b4a456..11f370e9572a 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.tsx @@ -5,19 +5,25 @@ * 2.0. */ -import React, { useState } from 'react'; +import dateMath from '@elastic/datemath'; +import React, { useMemo, useState } from 'react'; import type { EuiBasicTableColumn, OnTimeChangeProps } from '@elastic/eui'; import { + EuiCallOut, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, + EuiLink, EuiLoadingSpinner, EuiPanel, EuiSpacer, EuiSuperDatePicker, + EuiText, EuiToolTip, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useLicense } from '../../../common/hooks/use_license'; import { InvestigateInTimelineButton } from '../../../common/components/event_details/table/investigate_in_timeline_button'; import type { PrevalenceData } from '../../shared/hooks/use_prevalence'; import { usePrevalence } from '../../shared/hooks/use_prevalence'; @@ -63,16 +69,31 @@ export const PREVALENCE_TAB_ID = 'prevalence-details'; const DEFAULT_FROM = 'now-30d'; const DEFAULT_TO = 'now'; -const columns: Array> = [ +interface PrevalenceDetailsRow extends PrevalenceData { + /** + * From datetime selected in the date picker to pass to timeline + */ + from: string; + /** + * To datetime selected in the date picker to pass to timeline + */ + to: string; +} + +const columns: Array> = [ { field: 'field', name: PREVALENCE_TABLE_FIELD_COLUMN_TITLE, 'data-test-subj': PREVALENCE_DETAILS_TABLE_FIELD_CELL_TEST_ID, + render: (field: string) => {field}, + width: '20%', }, { field: 'value', name: PREVALENCE_TABLE_VALUE_COLUMN_TITLE, 'data-test-subj': PREVALENCE_DETAILS_TABLE_VALUE_CELL_TEST_ID, + render: (value: string) => {value}, + width: '20%', }, { name: ( @@ -84,7 +105,7 @@ const columns: Array> = [ ), 'data-test-subj': PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID, - render: (data: PrevalenceData) => { + render: (data: PrevalenceDetailsRow) => { const dataProviders = [ getDataProvider(data.field, `timeline-indicator-${data.field}-${data.value}`, data.value), ]; @@ -93,6 +114,7 @@ const columns: Array> = [ asEmptyButton={true} dataProviders={dataProviders} filters={[]} + timeRange={{ kind: 'absolute', from: data.from, to: data.to }} > <>{data.alertCount} @@ -112,7 +134,7 @@ const columns: Array> = [ ), 'data-test-subj': PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID, - render: (data: PrevalenceData) => { + render: (data: PrevalenceDetailsRow) => { const dataProviders = [ { ...getDataProvider( @@ -136,6 +158,7 @@ const columns: Array> = [ asEmptyButton={true} dataProviders={dataProviders} filters={[]} + timeRange={{ kind: 'absolute', from: data.from, to: data.to }} keepDataView // changing dataview from only detections to include non-alerts docs > <>{data.docCount} @@ -158,10 +181,7 @@ const columns: Array> = [ ), 'data-test-subj': PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID, render: (hostPrevalence: number) => ( - <> - {Math.round(hostPrevalence * 100)} - {'%'} - + {`${Math.round(hostPrevalence * 100)}%`} ), width: '10%', }, @@ -177,10 +197,7 @@ const columns: Array> = [ ), 'data-test-subj': PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID, render: (userPrevalence: number) => ( - <> - {Math.round(userPrevalence * 100)} - {'%'} - + {`${Math.round(userPrevalence * 100)}%`} ), width: '10%', }, @@ -193,12 +210,38 @@ export const PrevalenceDetails: React.FC = () => { const { browserFields, dataFormattedForFieldBrowser, eventId, investigationFields } = useLeftPanelContext(); + const isPlatinumPlus = useLicense().isPlatinumPlus(); + + // these two are used by the usePrevalence hook to fetch the data const [start, setStart] = useState(DEFAULT_FROM); const [end, setEnd] = useState(DEFAULT_TO); - const onTimeChange = ({ start: s, end: e }: OnTimeChangeProps) => { + // these two are used to pass to timeline + const [absoluteStart, setAbsoluteStart] = useState( + (dateMath.parse(DEFAULT_FROM) || new Date()).toISOString() + ); + const [absoluteEnd, setAbsoluteEnd] = useState( + (dateMath.parse(DEFAULT_TO) || new Date()).toISOString() + ); + + // TODO update the logic to use a single set of start/end dates + // currently as we're using this InvestigateInTimelineButton component we need to pass the timeRange + // as an AbsoluteTimeRange, which requires from/to values + const onTimeChange = ({ start: s, end: e, isInvalid }: OnTimeChangeProps) => { + if (isInvalid) return; + setStart(s); setEnd(e); + + const from = dateMath.parse(s); + if (from && from.isValid()) { + setAbsoluteStart(from.toISOString()); + } + + const to = dateMath.parse(e); + if (to && to.isValid()) { + setAbsoluteEnd(to.toISOString()); + } }; const { loading, error, data } = usePrevalence({ @@ -210,6 +253,12 @@ export const PrevalenceDetails: React.FC = () => { }, }); + // add timeRange to pass it down to timeline + const items = useMemo( + () => data.map((item) => ({ ...item, from: absoluteStart, to: absoluteEnd })), + [data, absoluteStart, absoluteEnd] + ); + if (loading) { return ( { ); } + const upsell = ( + <> + + + + + ), + }} + /> + + + + ); + return ( <> + {!isPlatinumPlus && upsell} { {data.length > 0 ? ( diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/response_details.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/response_details.tsx index 07153217262b..6281a34e0d78 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/response_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/response_details.tsx @@ -69,7 +69,7 @@ export const ResponseDetails: React.FC = () => { values={{ editRuleLink: ( > = [ field: 'field', name: HIGHLIGHTED_FIELDS_FIELD_COLUMN, 'data-test-subj': 'fieldCell', + width: '50%', }, { field: 'description', name: HIGHLIGHTED_FIELDS_VALUE_COLUMN, 'data-test-subj': 'valueCell', + width: '50%', render: (description: { field: string; values: string[] | null | undefined; diff --git a/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_fetch_prevalence.ts b/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_fetch_prevalence.ts index 804784728c3f..3a0f5f824f4b 100644 --- a/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_fetch_prevalence.ts +++ b/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_fetch_prevalence.ts @@ -11,6 +11,9 @@ import { useQuery } from '@tanstack/react-query'; import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { createFetchData } from '../utils/fetch_data'; import { useKibana } from '../../../common/lib/kibana'; +import { useTimelineDataFilters } from '../../../timelines/containers/use_timeline_data_filters'; +import { isActiveTimeline } from '../../../helpers'; +import { SourcererScopeName } from '../../../common/store/sourcerer/model'; const QUERY_KEY = 'useFetchFieldValuePairWithAggregation'; @@ -99,7 +102,10 @@ export const useFetchPrevalence = ({ }, } = useKibana(); - const searchRequest = buildSearchRequest(highlightedFieldsFilters, from, to); + // retrieves detections and non-detections indices (for example, the alert security index from the current space and 'logs-*' indices) + const { selectedPatterns } = useTimelineDataFilters(isActiveTimeline(SourcererScopeName.default)); + + const searchRequest = buildSearchRequest(highlightedFieldsFilters, from, to, selectedPatterns); const { data, isLoading, isError } = useQuery( [QUERY_KEY, highlightedFieldsFilters, from, to], @@ -120,7 +126,8 @@ export const useFetchPrevalence = ({ const buildSearchRequest = ( highlightedFieldsFilters: Record, from: string, - to: string + to: string, + selectedPatterns: string[] ): IEsSearchRequest => { const query = buildEsQuery( undefined, @@ -146,14 +153,16 @@ const buildSearchRequest = ( ] ); - return buildAggregationSearchRequest(query, highlightedFieldsFilters); + return buildAggregationSearchRequest(query, highlightedFieldsFilters, selectedPatterns); }; const buildAggregationSearchRequest = ( query: QueryDslQueryContainer, - highlightedFieldsFilters: Record + highlightedFieldsFilters: Record, + selectedPatterns: string[] ): IEsSearchRequest => ({ params: { + index: selectedPatterns, body: { query, aggs: {