From be53e5f2f54576fb71a793859b1a03764323aefd Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Mon, 11 Apr 2022 15:34:58 -0400 Subject: [PATCH] [Security Solution] Rule Preview Table Follow-up (#128981) (cherry picked from commit 268470a4406c1d3428f65ac4f82526c7e00c2b3f) --- .../common/detection_engine/constants.ts | 2 +- .../common/types/timeline/index.ts | 2 + .../cypress/tasks/create_new_rule.ts | 4 +- .../event_details/alert_summary_view.test.tsx | 9 + .../components/event_details/columns.test.tsx | 13 ++ .../event_details/event_details.test.tsx | 15 ++ .../event_fields_browser.test.tsx | 19 ++ .../event_details/overview/index.test.tsx | 19 ++ .../utils/timeline/use_show_timeline.tsx | 2 +- .../alerts_table/default_config.tsx | 18 +- .../components/rules/rule_preview/helpers.ts | 2 +- .../components/rules/rule_preview/index.tsx | 10 +- .../rule_preview/preview_histogram.test.tsx | 90 +++------- .../rules/rule_preview/preview_histogram.tsx | 106 +++++------ .../preview_table_cell_renderer.test.tsx | 167 ++++++++++++++++++ .../preview_table_cell_renderer.tsx | 1 + .../rules/rule_preview/translations.ts | 21 ++- .../rule_preview/use_preview_histogram.tsx | 11 +- .../security_solution_detections/columns.ts | 51 ++++-- .../side_panel/event_details/index.test.tsx | 9 + .../timelines/common/types/timeline/index.ts | 2 + .../components/t_grid/integrated/index.tsx | 2 +- .../translations/translations/fr-FR.json | 3 - .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - .../detection_engine_api_integration/utils.ts | 4 +- 26 files changed, 418 insertions(+), 170 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_table_cell_renderer.test.tsx diff --git a/x-pack/plugins/security_solution/common/detection_engine/constants.ts b/x-pack/plugins/security_solution/common/detection_engine/constants.ts index b61cd34dc4790..6a6bf9df6d27a 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/constants.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/constants.ts @@ -6,7 +6,7 @@ */ export enum RULE_PREVIEW_INVOCATION_COUNT { - HOUR = 20, + HOUR = 12, DAY = 24, WEEK = 168, MONTH = 30, diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index d2e9c2a6715fe..caeeaa0c17bee 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -326,6 +326,7 @@ export enum TimelineId { casePage = 'timeline-case', test = 'test', // Reserved for testing purposes alternateTest = 'alternateTest', + rulePreview = 'rule-preview', } export const TimelineIdLiteralRt = runtimeTypes.union([ @@ -339,6 +340,7 @@ export const TimelineIdLiteralRt = runtimeTypes.union([ runtimeTypes.literal(TimelineId.networkPageExternalAlerts), runtimeTypes.literal(TimelineId.active), runtimeTypes.literal(TimelineId.test), + runtimeTypes.literal(TimelineId.rulePreview), ]); export type TimelineIdLiteral = runtimeTypes.TypeOf; diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index 4ea8f4ce0ff91..3713f7c62ade4 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -327,9 +327,9 @@ export const fillDefineEqlRuleAndContinue = (rule: CustomRule) => { cy.get(PREVIEW_HISTOGRAM) .invoke('text') .then((text) => { - if (text !== 'Hits') { + if (text !== 'Rule Preview') { cy.get(RULES_CREATION_PREVIEW).find(QUERY_PREVIEW_BUTTON).click({ force: true }); - cy.get(PREVIEW_HISTOGRAM).should('contain.text', 'Hits'); + cy.get(PREVIEW_HISTOGRAM).should('contain.text', 'Rule Preview'); } }); cy.get(TOAST_ERROR).should('not.exist'); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx index 650b915f50214..fe8a3d00a6364 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx @@ -82,6 +82,15 @@ describe('AlertSummaryView', () => { expect(queryAllByTestId('hover-actions-filter-for').length).toEqual(0); }); + test('it does NOT render the action cell when readOnly is passed', () => { + const { queryAllByTestId } = render( + + + + ); + expect(queryAllByTestId('hover-actions-filter-for').length).toEqual(0); + }); + test("render no investigation guide if it doesn't exist", async () => { (useRuleWithFallback as jest.Mock).mockReturnValue({ rule: { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/columns.test.tsx index a37157905bef9..be197499a700b 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/columns.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/columns.test.tsx @@ -136,5 +136,18 @@ describe('getColumns', () => { ).toEqual('hover-actions-copy-button'); }); }); + + describe('does not render hover actions when readOnly prop is passed', () => { + test('it renders a filter for (+) button', () => { + actionsColumn = getColumns({ ...defaultProps, isReadOnly: true })[0] as Column; + const wrapper = mount( + {actionsColumn.render(testValue, testData)} + ) as ReactWrapper; + + expect(wrapper.find('[data-test-subj="hover-actions-filter-for"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="hover-actions-filter-out"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="more-actions-agent.id"]').exists()).toBeFalsy(); + }); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx index 14910c77d198c..f7df157ed4602 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx @@ -140,5 +140,20 @@ describe('EventDetails', () => { alertsWrapper.find('[data-test-subj="threatIntelTab"]').first().simulate('click'); expect(alertsWrapper.find('[data-test-subj="no-enrichments-found"]').exists()).toEqual(true); }); + it('does not render if readOnly prop is passed', async () => { + const newProps = { ...defaultProps, isReadOnly: true }; + wrapper = mount( + + + + ) as ReactWrapper; + alertsWrapper = mount( + + + + ) as ReactWrapper; + await waitFor(() => wrapper.update()); + expect(alertsWrapper.find('[data-test-subj="threatIntelTab"]').exists()).toBeFalsy(); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx index a05a9e36a24e9..dcb28eafb6ef8 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx @@ -132,6 +132,25 @@ describe('EventFieldsBrowser', () => { expect(wrapper.find('[data-test-subj="more-actions-@timestamp"]').exists()).toBeTruthy(); }); + test('it does not render hover actions when readOnly prop is passed', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="hover-actions-filter-for"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="hover-actions-filter-out"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="more-actions-@timestamp"]').exists()).toBeFalsy(); + }); + test('it renders a column toggle button', () => { const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/overview/index.test.tsx index 0f241bace7663..52e038fa07c56 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/overview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/overview/index.test.tsx @@ -29,6 +29,20 @@ describe('Event Details Overview Cards', () => { getByText('Rule'); }); + it('renders only readOnly cards', () => { + const { getByText, queryByText } = render( + + + + ); + + getByText('Severity'); + getByText('Risk Score'); + + expect(queryByText('Status')).not.toBeInTheDocument(); + expect(queryByText('Rule')).not.toBeInTheDocument(); + }); + it('renders all cards it has data for', () => { const { getByText, queryByText } = render( @@ -194,3 +208,8 @@ const propsWithoutSeverity = { browserFields: { kibana: { fields: fieldsWithoutSeverity } }, data: dataWithoutSeverity, }; + +const propsWithReadOnly = { + ...props, + isReadOnly: true, +}; diff --git a/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx b/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx index f3ee9cbd81c52..e24a4d0025aee 100644 --- a/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx +++ b/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx @@ -8,7 +8,7 @@ import { useState, useEffect } from 'react'; import { useRouteSpy } from '../route/use_route_spy'; -const hideTimelineForRoutes = [`/cases/configure`, '/administration']; +const hideTimelineForRoutes = [`/cases/configure`, '/administration', 'rules/create']; export const useShowTimeline = () => { const [{ pageName, pathName }] = useRouteSpy(); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index 7dc3561628193..8ae7e358f280d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -16,7 +16,10 @@ import { RowRendererId } from '../../../../common/types/timeline'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { SubsetTimelineModel } from '../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; -import { columns } from '../../configurations/security_solution_detections/columns'; +import { + columns, + rulePreviewColumns, +} from '../../configurations/security_solution_detections/columns'; export const buildAlertStatusFilter = (status: Status): Filter[] => { const combinedQuery = @@ -156,6 +159,19 @@ export const alertsDefaultModel: SubsetTimelineModel = { excludedRowRendererIds: Object.values(RowRendererId), }; +export const alertsPreviewDefaultModel: SubsetTimelineModel = { + ...alertsDefaultModel, + columns: rulePreviewColumns, + defaultColumns: rulePreviewColumns, + sort: [ + { + columnId: 'kibana.alert.original_time', + columnType: 'number', + sortDirection: 'desc', + }, + ], +}; + export const requiredFieldsForActions = [ '@timestamp', 'kibana.alert.workflow_status', diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/helpers.ts index f4e58c0d34c74..a2c4d2b14cdc9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/helpers.ts @@ -234,7 +234,7 @@ export const getIsRulePreviewDisabled = ({ if (ruleType === 'machine_learning') { return machineLearningJobId.length === 0; } - if (ruleType === 'eql') { + if (ruleType === 'eql' || ruleType === 'query' || ruleType === 'threshold') { return queryBar.query.query.length === 0; } return false; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.tsx index a870b837a7d33..1286f6d5758c7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.tsx @@ -29,6 +29,13 @@ import { LoadingHistogram } from './loading_histogram'; import { FieldValueThreshold } from '../threshold_input'; import { isJobStarted } from '../../../../../common/machine_learning/helpers'; +const HelpTextComponent = ( + + {i18n.QUERY_PREVIEW_HELP_TEXT} + {i18n.QUERY_PREVIEW_DISCLAIMER} + +); + export interface RulePreviewProps { index: string[]; isDisabled: boolean; @@ -116,7 +123,7 @@ const RulePreviewComponent: React.FC = ({ <> = ({ previewId={previewId} addNoiseWarning={addNoiseWarning} spaceId={spaceId} - threshold={threshold} index={index} /> )} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.test.tsx index bf174984a3492..1f16ddf3f845e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { mount } from 'enzyme'; +import { render } from '@testing-library/react'; import * as i18n from '../rule_preview/translations'; import { useGlobalTime } from '../../../../common/containers/use_global_time'; @@ -14,10 +14,12 @@ import { TestProviders } from '../../../../common/mock'; import { usePreviewHistogram } from './use_preview_histogram'; import { PreviewHistogram } from './preview_histogram'; +import { ALL_VALUES_ZEROS_TITLE } from '../../../../common/components/charts/translation'; +jest.mock('../../../../common/lib/kibana'); jest.mock('../../../../common/containers/use_global_time'); jest.mock('./use_preview_histogram'); -jest.mock('../../../../common/lib/kibana'); +jest.mock('../../../../common/components/url_state/normalize_time_range.ts'); describe('PreviewHistogram', () => { const mockSetQuery = jest.fn(); @@ -35,9 +37,9 @@ describe('PreviewHistogram', () => { jest.clearAllMocks(); }); - test('it renders loader when isLoading is true', () => { + describe('when there is no data', () => { (usePreviewHistogram as jest.Mock).mockReturnValue([ - true, + false, { inspect: { dsl: [], response: [] }, totalCount: 1, @@ -47,42 +49,38 @@ describe('PreviewHistogram', () => { }, ]); - const wrapper = mount( - - - - ); + test('it renders an empty histogram and table', () => { + const wrapper = render( + + + + ); - expect(wrapper.find('[data-test-subj="preview-histogram-loading"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="header-section-subtitle"]').text()).toEqual( - i18n.QUERY_PREVIEW_SUBTITLE_LOADING - ); + expect(wrapper.findByText('hello grid')).toBeTruthy(); + expect(wrapper.findByText(ALL_VALUES_ZEROS_TITLE)).toBeTruthy(); + }); }); - test('it configures data and subtitle', () => { + test('it renders loader when isLoading is true', () => { (usePreviewHistogram as jest.Mock).mockReturnValue([ - false, + true, { inspect: { dsl: [], response: [] }, - totalCount: 9154, + totalCount: 1, refetch: jest.fn(), - data: [ - { x: 1602247050000, y: 2314, g: 'All others' }, - { x: 1602247162500, y: 3471, g: 'All others' }, - { x: 1602247275000, y: 3369, g: 'All others' }, - ], + data: [], buckets: [], }, ]); - const wrapper = mount( + const wrapper = render( { ); - expect(wrapper.find('[data-test-subj="preview-histogram-loading"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="header-section-subtitle"]').text()).toEqual( - i18n.QUERY_PREVIEW_TITLE(9154) - ); - expect( - ( - wrapper.find('[data-test-subj="preview-histogram-bar-chart"]').props() as { - barChart: unknown; - } - ).barChart - ).toEqual([ - { - key: 'hits', - value: [ - { - g: 'All others', - x: 1602247050000, - y: 2314, - }, - { - g: 'All others', - x: 1602247162500, - y: 3471, - }, - { - g: 'All others', - x: 1602247275000, - y: 3369, - }, - ], - }, - ]); + expect(wrapper.findByTestId('preview-histogram-loading')).toBeTruthy(); + expect(wrapper.findByText(i18n.QUERY_PREVIEW_SUBTITLE_LOADING)).toBeTruthy(); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx index 326811783fc4a..2119ab75ee67b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx @@ -11,19 +11,20 @@ import { Unit } from '@kbn/datemath'; import { EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer, EuiLoadingChart } from '@elastic/eui'; import styled from 'styled-components'; import { Type } from '@kbn/securitysolution-io-ts-alerting-types'; +import { useDispatch, useSelector } from 'react-redux'; +import { eventsViewerSelector } from '../../../../common/components/events_viewer/selectors'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { useKibana } from '../../../../common/lib/kibana'; import * as i18n from './translations'; import { useGlobalTime } from '../../../../common/containers/use_global_time'; -import { getHistogramConfig, getThresholdHistogramConfig, isNoisy } from './helpers'; +import { getHistogramConfig, isNoisy } from './helpers'; import { ChartSeriesConfigs, ChartSeriesData } from '../../../../common/components/charts/common'; import { Panel } from '../../../../common/components/panel'; import { HeaderSection } from '../../../../common/components/header_section'; import { BarChart } from '../../../../common/components/charts/barchart'; import { usePreviewHistogram } from './use_preview_histogram'; import { formatDate } from '../../../../common/components/super_date_picker'; -import { FieldValueThreshold } from '../threshold_input'; -import { alertsDefaultModel } from '../../alerts_table/default_config'; +import { alertsPreviewDefaultModel } from '../../alerts_table/default_config'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; import { defaultRowRenderers } from '../../../../timelines/components/timeline/body/renderers'; import { TimelineId } from '../../../../../common/types'; @@ -35,6 +36,8 @@ import { PreviewRenderCellValue } from './preview_table_cell_renderer'; import { getPreviewTableControlColumn } from './preview_table_control_columns'; import { useGlobalFullScreen } from '../../../../common/containers/use_full_screen'; import { InspectButtonContainer } from '../../../../common/components/inspect'; +import { timelineActions } from '../../../../timelines/store/timeline'; +import { State } from '../../../../common/store'; const LoadingChart = styled(EuiLoadingChart)` display: block; @@ -55,7 +58,6 @@ interface PreviewHistogramProps { previewId: string; addNoiseWarning: () => void; spaceId: string; - threshold?: FieldValueThreshold; ruleType: Type; index: string[]; } @@ -67,10 +69,10 @@ export const PreviewHistogram = ({ previewId, addNoiseWarning, spaceId, - threshold, ruleType, index, }: PreviewHistogramProps) => { + const dispatch = useDispatch(); const { setQuery, isInitializing } = useGlobalTime(); const { timelines: timelinesUi, cases } = useKibana().services; const from = useMemo(() => `now-1${timeFrame}`, [timeFrame]); @@ -78,34 +80,36 @@ export const PreviewHistogram = ({ const startDate = useMemo(() => formatDate(from), [from]); const endDate = useMemo(() => formatDate(to), [to]); const isEqlRule = useMemo(() => ruleType === 'eql', [ruleType]); - const isThresholdRule = useMemo(() => ruleType === 'threshold', [ruleType]); + const isMlRule = useMemo(() => ruleType === 'machine_learning', [ruleType]); - const [isLoading, { data, inspect, totalCount, refetch, buckets }] = usePreviewHistogram({ + const [isLoading, { data, inspect, totalCount, refetch }] = usePreviewHistogram({ previewId, startDate, endDate, spaceId, - threshold: isThresholdRule ? threshold : undefined, index, ruleType, }); const { - columns, - dataProviders, - deletedEventIds, - kqlMode, - itemsPerPage, - itemsPerPageOptions, - graphEventId, - sort, - } = alertsDefaultModel; + timeline: { + columns, + dataProviders, + defaultColumns, + deletedEventIds, + itemsPerPage, + itemsPerPageOptions, + kqlMode, + sort, + } = alertsPreviewDefaultModel, + } = useSelector((state: State) => eventsViewerSelector(state, TimelineId.rulePreview)); const { browserFields, docValueFields, indexPattern, runtimeMappings, + dataViewId: selectedDataViewId, loading: isLoadingIndexPattern, } = useSourcererDataView(SourcererScopeName.detections); @@ -129,42 +133,28 @@ export const PreviewHistogram = ({ } }, [setQuery, inspect, isLoading, isInitializing, refetch, previewId]); + useEffect(() => { + dispatch( + timelineActions.createTimeline({ + columns, + dataViewId: selectedDataViewId, + defaultColumns, + id: TimelineId.rulePreview, + indexNames: [`${DEFAULT_PREVIEW_INDEX}-${spaceId}`], + itemsPerPage, + sort, + }) + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const barConfig = useMemo( (): ChartSeriesConfigs => getHistogramConfig(endDate, startDate, !isEqlRule), [endDate, startDate, isEqlRule] ); - const thresholdBarConfig = useMemo((): ChartSeriesConfigs => getThresholdHistogramConfig(), []); - const chartData = useMemo((): ChartSeriesData[] => [{ key: 'hits', value: data }], [data]); - const { thresholdChartData, thresholdTotalCount } = useMemo((): { - thresholdChartData: ChartSeriesData[]; - thresholdTotalCount: number; - } => { - const total = buckets.length; - const dataBuckets = buckets.map<{ x: string; y: number; g: string }>( - ({ key, doc_count: docCount }) => ({ - x: key, - y: docCount, - g: key, - }) - ); - return { - thresholdChartData: [{ key: 'hits', value: dataBuckets }], - thresholdTotalCount: total, - }; - }, [buckets]); - - const subtitle = useMemo( - (): string => - isLoading - ? i18n.QUERY_PREVIEW_SUBTITLE_LOADING - : isThresholdRule - ? i18n.QUERY_PREVIEW_THRESHOLD_WITH_FIELD_TITLE(thresholdTotalCount) - : i18n.QUERY_PREVIEW_TITLE(totalCount), - [isLoading, totalCount, thresholdTotalCount, isThresholdRule] - ); const CasesContext = cases.ui.getCasesContext(); return ( @@ -176,7 +166,6 @@ export const PreviewHistogram = ({ id={`${ID}-${previewId}`} title={i18n.QUERY_GRAPH_HITS_TITLE} titleSize="xs" - subtitle={subtitle} /> @@ -184,8 +173,8 @@ export const PreviewHistogram = ({ ) : ( )} @@ -194,7 +183,11 @@ export const PreviewHistogram = ({ <> -

{i18n.QUERY_PREVIEW_DISCLAIMER_MAX_SIGNALS}

+

+ {isMlRule + ? i18n.ML_PREVIEW_HISTOGRAM_DISCLAIMER + : i18n.PREVIEW_HISTOGRAM_DISCLAIMER} +

@@ -213,13 +206,12 @@ export const PreviewHistogram = ({ deletedEventIds, disabledCellActions: FIELDS_WITHOUT_CELL_ACTIONS, docValueFields, - end: to, - entityType: 'alerts', + end: endDate, + entityType: 'events', filters: [], globalFullScreen, - graphEventId, hasAlertsCrud: false, - id: TimelineId.detectionsPage, + id: TimelineId.rulePreview, indexNames: [`${DEFAULT_PREVIEW_INDEX}-${spaceId}`], indexPattern, isLive: false, @@ -233,7 +225,7 @@ export const PreviewHistogram = ({ runtimeMappings, setQuery: () => {}, sort, - start: from, + start: startDate, tGridEventRenderedViewEnabled, type: 'embedded', leadingControlColumns: getPreviewTableControlColumn(1.5), @@ -242,11 +234,11 @@ export const PreviewHistogram = ({ diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_table_cell_renderer.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_table_cell_renderer.test.tsx new file mode 100644 index 0000000000000..37b03dbc3fd88 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_table_cell_renderer.test.tsx @@ -0,0 +1,167 @@ +/* + * 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 { mount } from 'enzyme'; +import { cloneDeep } from 'lodash/fp'; +import React from 'react'; + +import { mockBrowserFields } from '../../../../common/containers/source/mock'; +import { DragDropContextWrapper } from '../../../../common/components/drag_and_drop/drag_drop_context_wrapper'; +import { defaultHeaders, mockTimelineData, TestProviders } from '../../../../common/mock'; +import { PreviewTableCellRenderer } from './preview_table_cell_renderer'; +import { getColumnRenderer } from '../../../../timelines/components/timeline/body/renderers/get_column_renderer'; +import { DroppableWrapper } from '../../../../common/components/drag_and_drop/droppable_wrapper'; +import { BrowserFields } from '../../../../../../timelines/common/search_strategy'; +import { Ecs } from '../../../../../common/ecs'; +import { columnRenderers } from '../../../../timelines/components/timeline/body/renderers'; + +jest.mock('../../../../common/lib/kibana'); +jest.mock('../../../../timelines/components/timeline/body/renderers/get_column_renderer'); + +const getColumnRendererMock = getColumnRenderer as jest.Mock; +const mockImplementation = { + renderColumn: jest.fn(), +}; + +describe('PreviewTableCellRenderer', () => { + const columnId = '@timestamp'; + const eventId = '_id-123'; + const isExpandable = true; + const isExpanded = true; + const linkValues = ['foo', 'bar', '@baz']; + const rowIndex = 3; + const colIndex = 0; + const setCellProps = jest.fn(); + const timelineId = 'test'; + const ecsData = {} as Ecs; + const browserFields = {} as BrowserFields; + + beforeEach(() => { + jest.clearAllMocks(); + getColumnRendererMock.mockImplementation(() => mockImplementation); + }); + + test('it invokes `getColumnRenderer` with the expected arguments', () => { + const data = cloneDeep(mockTimelineData[0].data); + const header = cloneDeep(defaultHeaders[0]); + const isDetails = true; + + mount( + + + + + + + + ); + + expect(getColumnRenderer).toBeCalledWith(header.id, columnRenderers, data); + }); + + test('if in tgrid expanded value, it invokes `renderColumn` with the expected arguments', () => { + const data = cloneDeep(mockTimelineData[0].data); + const header = cloneDeep(defaultHeaders[0]); + const isDetails = true; + const truncate = isDetails ? false : true; + + mount( + + + + + + + + ); + + expect(mockImplementation.renderColumn).toBeCalledWith({ + asPlainText: false, + browserFields, + columnName: header.id, + ecsData, + eventId, + field: header, + isDetails, + isDraggable: true, + linkValues, + rowRenderers: undefined, + timelineId, + truncate, + values: ['2018-11-05T19:03:25.937Z'], + }); + }); + + test('if in tgrid expanded value, it does not render any actions', () => { + const data = cloneDeep(mockTimelineData[0].data); + const header = cloneDeep(defaultHeaders[1]); + const isDetails = true; + const id = 'event.severity'; + const wrapper = mount( + + + + + + + + ); + + expect( + wrapper.find('[data-test-subj="data-grid-expanded-cell-value-actions"]').exists() + ).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_table_cell_renderer.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_table_cell_renderer.tsx index ab56bd3fbfe0d..0e9df27827e47 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_table_cell_renderer.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_table_cell_renderer.tsx @@ -89,6 +89,7 @@ export const PreviewTableCellRenderer: React.FC = ({ const styledContentClassName = isDetails ? 'eui-textBreakWord' : 'eui-displayInlineBlock eui-textTruncate'; + return ( <> diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/translations.ts index 3acb533913cfd..81ff4b8cfc440 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/translations.ts @@ -54,7 +54,7 @@ export const QUERY_PREVIEW_LABEL = i18n.translate( export const QUERY_PREVIEW_HELP_TEXT = i18n.translate( 'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewHelpText', { - defaultMessage: 'Select a timeframe of data to preview query results', + defaultMessage: 'Select a timeframe of data to preview query results.', } ); @@ -73,9 +73,9 @@ export const THRESHOLD_QUERY_GRAPH_COUNT = i18n.translate( ); export const QUERY_GRAPH_HITS_TITLE = i18n.translate( - 'xpack.securitySolution.detectionEngine.queryPreview.queryGraphHitsTitle', + 'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewTitle', { - defaultMessage: 'Hits', + defaultMessage: 'Rule Preview', } ); @@ -124,18 +124,25 @@ export const QUERY_PREVIEW_ERROR = i18n.translate( ); export const QUERY_PREVIEW_DISCLAIMER = i18n.translate( - 'xpack.securitySolution.detectionEngine.queryPreview.queryGraphDisclaimer', + 'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewDisclaimer', { defaultMessage: 'Note: This preview excludes effects of rule exceptions and timestamp overrides.', } ); -export const QUERY_PREVIEW_DISCLAIMER_MAX_SIGNALS = i18n.translate( - 'xpack.securitySolution.detectionEngine.queryPreview.queryGraphDisclaimerEql', +export const PREVIEW_HISTOGRAM_DISCLAIMER = i18n.translate( + 'xpack.securitySolution.detectionEngine.queryPreview.histogramDisclaimer', { defaultMessage: - 'Note: This preview excludes effects of rule exceptions and timestamp overrides, and is limited to 100 results.', + 'Note: Alerts with multiple event.category values will be counted more than once.', + } +); + +export const ML_PREVIEW_HISTOGRAM_DISCLAIMER = i18n.translate( + 'xpack.securitySolution.detectionEngine.queryPreview.mlHistogramDisclaimer', + { + defaultMessage: 'Note: Alerts with multiple host.name values will be counted more than once.', } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/use_preview_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/use_preview_histogram.tsx index 0d63092ee4eef..d336c75bb5f49 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/use_preview_histogram.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/use_preview_histogram.tsx @@ -13,14 +13,12 @@ import { getEsQueryConfig } from '../../../../../../../../src/plugins/data/commo import { useKibana } from '../../../../common/lib/kibana'; import { QUERY_PREVIEW_ERROR } from './translations'; import { DEFAULT_PREVIEW_INDEX } from '../../../../../common/constants'; -import { FieldValueThreshold } from '../threshold_input'; interface PreviewHistogramParams { previewId: string | undefined; endDate: string; startDate: string; spaceId: string; - threshold?: FieldValueThreshold; index: string[]; ruleType: Type; } @@ -30,7 +28,6 @@ export const usePreviewHistogram = ({ startDate, endDate, spaceId, - threshold, index, ruleType, }: PreviewHistogramParams) => { @@ -47,9 +44,8 @@ export const usePreviewHistogram = ({ }); const stackByField = useMemo(() => { - const stackByDefault = ruleType === 'machine_learning' ? 'host.name' : 'event.category'; - return threshold?.field[0] ?? stackByDefault; - }, [threshold, ruleType]); + return ruleType === 'machine_learning' ? 'host.name' : 'event.category'; + }, [ruleType]); const matrixHistogramRequest = useMemo(() => { return { @@ -60,11 +56,10 @@ export const usePreviewHistogram = ({ indexNames: [`${DEFAULT_PREVIEW_INDEX}-${spaceId}`], stackByField, startDate, - threshold, includeMissingData: false, skip: error != null, }; - }, [startDate, endDate, filterQuery, spaceId, error, threshold, stackByField]); + }, [startDate, endDate, filterQuery, spaceId, error, stackByField]); return useMatrixHistogramCombined(matrixHistogramRequest); }; diff --git a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts index a7942642b830f..b542fa7d40c4a 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts +++ b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts @@ -16,25 +16,9 @@ import { import * as i18n from '../../components/alerts_table/translations'; -/** - * columns implements a subset of `EuiDataGrid`'s `EuiDataGridColumn` interface, - * plus additional TGrid column properties - */ -export const columns: Array< +const baseColumns: Array< Pick & ColumnHeaderOptions > = [ - { - columnHeaderType: defaultColumnHeaderType, - id: '@timestamp', - initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH + 10, - }, - { - columnHeaderType: defaultColumnHeaderType, - displayAsText: i18n.ALERTS_HEADERS_RULE, - id: 'kibana.alert.rule.name', - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - linkField: 'kibana.alert.rule.uuid', - }, { columnHeaderType: defaultColumnHeaderType, displayAsText: i18n.ALERTS_HEADERS_SEVERITY, @@ -78,3 +62,36 @@ export const columns: Array< id: 'destination.ip', }, ]; + +/** + * columns implements a subset of `EuiDataGrid`'s `EuiDataGridColumn` interface, + * plus additional TGrid column properties + */ +export const columns: Array< + Pick & ColumnHeaderOptions +> = [ + { + columnHeaderType: defaultColumnHeaderType, + id: '@timestamp', + initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH + 10, + }, + { + columnHeaderType: defaultColumnHeaderType, + displayAsText: i18n.ALERTS_HEADERS_RULE, + id: 'kibana.alert.rule.name', + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + linkField: 'kibana.alert.rule.uuid', + }, + ...baseColumns, +]; + +export const rulePreviewColumns: Array< + Pick & ColumnHeaderOptions +> = [ + { + columnHeaderType: defaultColumnHeaderType, + id: 'kibana.alert.original_time', + initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH + 10, + }, + ...baseColumns, +]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.test.tsx index bdb896e31cfa3..5147301b4ea5d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.test.tsx @@ -167,4 +167,13 @@ describe('event details footer component', () => { ); expect(wrapper.getByTestId('side-panel-flyout-footer')).toBeTruthy(); }); + test("it doesn't render the take action dropdown when readOnly prop is passed", () => { + const wrapper = render( + + + + ); + const element = wrapper.queryByTestId('side-panel-flyout-footer'); + expect(element).toBeNull(); + }); }); diff --git a/x-pack/plugins/timelines/common/types/timeline/index.ts b/x-pack/plugins/timelines/common/types/timeline/index.ts index cac24f65d0fd2..867264fa81546 100644 --- a/x-pack/plugins/timelines/common/types/timeline/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/index.ts @@ -322,6 +322,7 @@ export enum TimelineId { casePage = 'timeline-case', test = 'test', // Reserved for testing purposes alternateTest = 'alternateTest', + rulePreview = 'rule-preview', } export const TimelineIdLiteralRt = runtimeTypes.union([ @@ -333,6 +334,7 @@ export const TimelineIdLiteralRt = runtimeTypes.union([ runtimeTypes.literal(TimelineId.networkPageExternalAlerts), runtimeTypes.literal(TimelineId.active), runtimeTypes.literal(TimelineId.test), + runtimeTypes.literal(TimelineId.rulePreview), ]); export type TimelineIdLiteral = runtimeTypes.TypeOf; diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx index 6f74fd1d2cb37..2991d80c7af0a 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx @@ -113,7 +113,7 @@ export interface TGridIntegratedProps { filterStatus?: AlertStatus; globalFullScreen: boolean; // If truthy, the graph viewer (Resolver) is showing - graphEventId: string | undefined; + graphEventId?: string; graphOverlay?: React.ReactNode; hasAlertsCrud: boolean; height?: number; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 677aad1eedd37..205310a0b93f8 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -20739,9 +20739,6 @@ "xpack.securitySolution.detectionEngine.pageTitle": "Moteur de détection", "xpack.securitySolution.detectionEngine.panelSubtitleShowing": "Affichant", "xpack.securitySolution.detectionEngine.queryPreview.queryGraphCountLabel": "Compte", - "xpack.securitySolution.detectionEngine.queryPreview.queryGraphDisclaimer": "Remarque : cet aperçu exclut les effets d'exceptions aux règles et les remplacements d'horodatages.", - "xpack.securitySolution.detectionEngine.queryPreview.queryGraphDisclaimerEql": "Remarque : cet aperçu exclut les effets d'exceptions aux règles et les remplacements d'horodatages, et est limité à 100 résultats.", - "xpack.securitySolution.detectionEngine.queryPreview.queryGraphHitsTitle": "Résultats", "xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewError": "Erreur de récupération de l'aperçu", "xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewInspectTitle": "aperçu de la recherche", "xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewNoiseWarning": "Avertissement de bruit : cette règle peut générer beaucoup de bruit. Envisagez d'affiner votre recherche. La base est une progression linéaire comportant 1 alerte par heure.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 46578d781ad67..c7a1fbfc47212 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -23715,9 +23715,6 @@ "xpack.securitySolution.detectionEngine.pageTitle": "検出エンジン", "xpack.securitySolution.detectionEngine.panelSubtitleShowing": "表示中", "xpack.securitySolution.detectionEngine.queryPreview.queryGraphCountLabel": "カウント", - "xpack.securitySolution.detectionEngine.queryPreview.queryGraphDisclaimer": "注:このプレビューは、ルール例外とタイムスタンプオーバーライドの効果を除外します。", - "xpack.securitySolution.detectionEngine.queryPreview.queryGraphDisclaimerEql": "注:このプレビューは、ルール例外とタイムスタンプオーバーライドの効果を除外します。結果は100件に制限されます。", - "xpack.securitySolution.detectionEngine.queryPreview.queryGraphHitsTitle": "ヒット数", "xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewError": "プレビュー取得エラー", "xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewInspectTitle": "クエリプレビュー", "xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewNoiseWarning": "ノイズ警告:このルールではノイズが多く生じる可能性があります。クエリを絞り込むことを検討してください。これは1時間ごとに1アラートという線形進行に基づいています。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a79cd68f884e5..28cfd51d678de 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -23742,9 +23742,6 @@ "xpack.securitySolution.detectionEngine.pageTitle": "检测引擎", "xpack.securitySolution.detectionEngine.panelSubtitleShowing": "正在显示", "xpack.securitySolution.detectionEngine.queryPreview.queryGraphCountLabel": "计数", - "xpack.securitySolution.detectionEngine.queryPreview.queryGraphDisclaimer": "注意:此预览不包括规则例外和时间戳覆盖的影响。", - "xpack.securitySolution.detectionEngine.queryPreview.queryGraphDisclaimerEql": "注意:此预览不包括规则例外和时间戳覆盖的影响,且仅显示 100 个结果。", - "xpack.securitySolution.detectionEngine.queryPreview.queryGraphHitsTitle": "命中数", "xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewError": "提取预览时出错", "xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewInspectTitle": "查询预览", "xpack.securitySolution.detectionEngine.queryPreview.queryGraphPreviewNoiseWarning": "噪音警告:此规则可能会导致大量噪音。考虑缩小您的查询范围。这基于每小时 1 条告警的线性级数。", diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index ebf3a7008cd57..de66002343212 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -116,12 +116,12 @@ export const getSimpleRule = (ruleId = 'rule-1', enabled = false): QueryCreateSc /** * This is a typical simple preview rule for testing that is easy for most basic testing * @param ruleId - * @param enabled The number of times the rule will be run through the executors. Defaulted to 20, + * @param enabled The number of times the rule will be run through the executors. Defaulted to 12, * the execution time for the default interval time of 5m. */ export const getSimplePreviewRule = ( ruleId = 'preview-rule-1', - invocationCount = 20 + invocationCount = 12 ): PreviewRulesSchema => ({ name: 'Simple Rule Query', description: 'Simple Rule Query',