diff --git a/x-pack/plugins/rule_registry/server/config.ts b/x-pack/plugins/rule_registry/server/config.ts index 4b691c15d1b3c7..7f3a3db42556ea 100644 --- a/x-pack/plugins/rule_registry/server/config.ts +++ b/x-pack/plugins/rule_registry/server/config.ts @@ -32,4 +32,3 @@ export const config: PluginConfigDescriptor = { export type RuleRegistryPluginConfig = TypeOf; export const INDEX_PREFIX = '.alerts' as const; -export const INDEX_PREFIX_FOR_BACKING_INDICES = '.internal.alerts' as const; diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.mock.ts b/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.mock.ts index 323adcc7566746..f4b314092de9c0 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.mock.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.mock.ts @@ -29,6 +29,7 @@ export const createRuleDataClientMock = ( indexName, kibanaVersion: '7.16.0', isWriteEnabled: jest.fn(() => true), + indexNameWithNamespace: jest.fn((namespace: string) => indexName + namespace), getReader: jest.fn((_options?: { namespace?: string }) => ({ search, diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.ts b/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.ts index 8ffe4e4db753f2..4aa0126cdabf85 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.ts @@ -54,6 +54,10 @@ export class RuleDataClient implements IRuleDataClient { return this.options.indexInfo.kibanaVersion; } + public indexNameWithNamespace(namespace: string): string { + return this.options.indexInfo.getPrimaryAlias(namespace); + } + private get writeEnabled(): boolean { return this._isWriteEnabled; } diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/types.ts b/x-pack/plugins/rule_registry/server/rule_data_client/types.ts index 5ddbd0035526d3..5fab32eb388680 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_client/types.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_client/types.ts @@ -14,6 +14,7 @@ import { TechnicalRuleDataFieldName } from '../../common/technical_rule_data_fie export interface IRuleDataClient { indexName: string; + indexNameWithNamespace(namespace: string): string; kibanaVersion: string; isWriteEnabled(): boolean; getReader(options?: { namespace?: string }): IRuleDataReader; diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index_info.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index_info.ts index 52fef63a732f0f..eca44c550411f2 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index_info.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index_info.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { INDEX_PREFIX, INDEX_PREFIX_FOR_BACKING_INDICES } from '../config'; +import { INDEX_PREFIX } from '../config'; import { IndexOptions } from './index_options'; import { joinWithDash } from './utils'; @@ -23,16 +23,16 @@ interface ConstructorOptions { export class IndexInfo { constructor(options: ConstructorOptions) { const { indexOptions, kibanaVersion } = options; - const { registrationContext, dataset } = indexOptions; + const { registrationContext, dataset, additionalPrefix } = indexOptions; this.indexOptions = indexOptions; this.kibanaVersion = kibanaVersion; - this.baseName = joinWithDash(INDEX_PREFIX, `${registrationContext}.${dataset}`); - this.basePattern = joinWithDash(this.baseName, '*'); - this.baseNameForBackingIndices = joinWithDash( - INDEX_PREFIX_FOR_BACKING_INDICES, + this.baseName = joinWithDash( + `${additionalPrefix ?? ''}${INDEX_PREFIX}`, `${registrationContext}.${dataset}` ); + this.basePattern = joinWithDash(this.baseName, '*'); + this.baseNameForBackingIndices = `.internal${this.baseName}`; } /** diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index_options.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index_options.ts index ba0961c7926a12..cdec7c609699d6 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index_options.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index_options.ts @@ -95,6 +95,17 @@ export interface IndexOptions { * @example '.siem-signals', undefined */ secondaryAlias?: string; + + /** + * Optional prefix name that will be prepended to indices in addition to + * primary dataset and context naming convention. + * + * Currently used only for creating a preview index for the purpose of + * previewing alerts from a rule. The documents are identical to alerts, but + * shouldn't exist on an alert index and shouldn't be queried together with + * real alerts in any way, because the rule that created them doesn't exist + */ + additionalPrefix?: string; } /** diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 917f3d6567052a..794533ae12582f 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -39,7 +39,7 @@ export const DEFAULT_APP_TIME_RANGE = 'securitySolution:timeDefaults' as const; export const DEFAULT_APP_REFRESH_INTERVAL = 'securitySolution:refreshIntervalDefaults' as const; export const DEFAULT_ALERTS_INDEX = '.alerts-security.alerts' as const; export const DEFAULT_SIGNALS_INDEX = '.siem-signals' as const; -export const DEFAULT_PREVIEW_INDEX = '.siem-preview-signals' as const; +export const DEFAULT_PREVIEW_INDEX = '.preview.alerts-security.alerts' as const; export const DEFAULT_LISTS_INDEX = '.lists' as const; export const DEFAULT_ITEMS_INDEX = '.items' as const; // The DEFAULT_MAX_SIGNALS value exists also in `x-pack/plugins/cases/common/constants.ts` @@ -252,8 +252,6 @@ export const DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL = export const DETECTION_ENGINE_RULES_BULK_ACTION = `${DETECTION_ENGINE_RULES_URL}/_bulk_action` as const; export const DETECTION_ENGINE_RULES_PREVIEW = `${DETECTION_ENGINE_RULES_URL}/preview` as const; -export const DETECTION_ENGINE_RULES_PREVIEW_INDEX_URL = - `${DETECTION_ENGINE_RULES_PREVIEW}/index` as const; /** * Internal detection engine routes diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_preview_index.tsx b/x-pack/plugins/security_solution/common/detection_engine/constants.ts similarity index 58% rename from x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_preview_index.tsx rename to x-pack/plugins/security_solution/common/detection_engine/constants.ts index 7a35e35acefe8c..7f3c8228006735 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_preview_index.tsx +++ b/x-pack/plugins/security_solution/common/detection_engine/constants.ts @@ -5,11 +5,9 @@ * 2.0. */ -import { useEffect } from 'react'; -import { createPreviewIndex } from './api'; - -export const usePreviewIndex = () => { - useEffect(() => { - createPreviewIndex(); - }, []); -}; +export enum RULE_PREVIEW_INVOCATION_COUNT { + HOUR = 20, + DAY = 24, + WEEK = 168, + MONTH = 30, +} diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts index c5f4e5631e5c8c..97079253606f1b 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts @@ -370,6 +370,7 @@ export const previewRulesSchema = t.intersection([ createTypeSpecific, t.type({ invocationCount: t.number }), ]); +export type PreviewRulesSchema = t.TypeOf; type UpdateSchema = SharedUpdateSchema & T; export type EqlUpdateSchema = UpdateSchema>; diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts index 02d8837261f2f8..81022a43ff6830 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts @@ -174,7 +174,7 @@ describe('Detection rules, threshold', () => { cy.get(ALERT_GRID_CELL).contains(rule.name); }); - it('Preview results of keyword using "host.name"', () => { + it.skip('Preview results of keyword using "host.name"', () => { rule.index = [...rule.index, '.siem-signals*']; createCustomRuleActivated(getNewRule()); @@ -188,7 +188,7 @@ describe('Detection rules, threshold', () => { cy.get(PREVIEW_HEADER_SUBTITLE).should('have.text', '3 unique hits'); }); - it('Preview results of "ip" using "source.ip"', () => { + it.skip('Preview results of "ip" using "source.ip"', () => { const previewRule: ThresholdRule = { ...rule, thresholdField: 'source.ip', diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts index aadaa5dfa0d88a..a3e5e8af3f5987 100644 --- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts @@ -104,9 +104,9 @@ export const DEFINE_INDEX_INPUT = export const EQL_TYPE = '[data-test-subj="eqlRuleType"]'; -export const EQL_QUERY_INPUT = '[data-test-subj="eqlQueryBarTextInput"]'; +export const PREVIEW_HISTOGRAM = '[data-test-subj="preview-histogram-panel"]'; -export const EQL_QUERY_PREVIEW_HISTOGRAM = '[data-test-subj="queryPreviewEqlHistogram"]'; +export const EQL_QUERY_INPUT = '[data-test-subj="eqlQueryBarTextInput"]'; export const EQL_QUERY_VALIDATION_SPINNER = '[data-test-subj="eql-validation-loading"]'; @@ -170,7 +170,7 @@ export const RISK_OVERRIDE = export const RULES_CREATION_FORM = '[data-test-subj="stepDefineRule"]'; -export const RULES_CREATION_PREVIEW = '[data-test-subj="ruleCreationQueryPreview"]'; +export const RULES_CREATION_PREVIEW = '[data-test-subj="rule-preview"]'; export const RULE_DESCRIPTION_INPUT = '[data-test-subj="detectionEngineStepAboutRuleDescription"] [data-test-subj="input"]'; 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 68449363b86435..538f95c3c0a80c 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 @@ -33,7 +33,6 @@ import { DEFAULT_RISK_SCORE_INPUT, DEFINE_CONTINUE_BUTTON, EQL_QUERY_INPUT, - EQL_QUERY_PREVIEW_HISTOGRAM, EQL_QUERY_VALIDATION_SPINNER, EQL_TYPE, FALSE_POSITIVES_INPUT, @@ -92,6 +91,7 @@ import { EMAIL_CONNECTOR_USER_INPUT, EMAIL_CONNECTOR_PASSWORD_INPUT, EMAIL_CONNECTOR_SERVICE_SELECTOR, + PREVIEW_HISTOGRAM, } from '../screens/create_new_rule'; import { TOAST_ERROR } from '../screens/shared'; import { SERVER_SIDE_EVENT_COUNT } from '../screens/timeline'; @@ -324,12 +324,12 @@ export const fillDefineEqlRuleAndContinue = (rule: CustomRule) => { .find(QUERY_PREVIEW_BUTTON) .should('not.be.disabled') .click({ force: true }); - cy.get(EQL_QUERY_PREVIEW_HISTOGRAM) + cy.get(PREVIEW_HISTOGRAM) .invoke('text') .then((text) => { if (text !== 'Hits') { cy.get(RULES_CREATION_PREVIEW).find(QUERY_PREVIEW_BUTTON).click({ force: true }); - cy.get(EQL_QUERY_PREVIEW_HISTOGRAM).should('contain.text', 'Hits'); + cy.get(PREVIEW_HISTOGRAM).should('contain.text', 'Hits'); } }); cy.get(TOAST_ERROR).should('not.exist'); diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts index b67505a66be440..e5da55f740033c 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts @@ -15,7 +15,7 @@ import { MatrixHistogramType } from '../../../../common/search_strategy/security import { UpdateDateRange } from '../charts/common'; import { GlobalTimeArgs } from '../../containers/use_global_time'; import { DocValueFields } from '../../../../common/search_strategy'; -import { Threshold } from '../../../detections/components/rules/query_preview'; +import { FieldValueThreshold } from '../../../detections/components/rules/threshold_input'; export type MatrixHistogramMappingTypes = Record< string, @@ -77,7 +77,7 @@ export interface MatrixHistogramQueryProps { stackByField: string; startDate: string; histogramType: MatrixHistogramType; - threshold?: Threshold; + threshold?: FieldValueThreshold; skip?: boolean; isPtrIncluded?: boolean; includeMissingData?: boolean; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.test.tsx deleted file mode 100644 index 2e6991f87ec5a7..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.test.tsx +++ /dev/null @@ -1,128 +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 { mount } from 'enzyme'; - -import * as i18n from '../rule_preview/translations'; -import { useGlobalTime } from '../../../../common/containers/use_global_time'; -import { TestProviders } from '../../../../common/mock'; -import { PreviewCustomQueryHistogram } from './custom_histogram'; - -jest.mock('../../../../common/containers/use_global_time'); - -describe('PreviewCustomQueryHistogram', () => { - const mockSetQuery = jest.fn(); - - beforeEach(() => { - (useGlobalTime as jest.Mock).mockReturnValue({ - from: '2020-07-07T08:20:18.966Z', - isInitializing: false, - to: '2020-07-08T08:20:18.966Z', - setQuery: mockSetQuery, - }); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - test('it renders loader when isLoading is true', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy(); - expect( - wrapper.find('[dataTestSubj="queryPreviewCustomHistogram"]').at(0).prop('subtitle') - ).toEqual(i18n.QUERY_PREVIEW_SUBTITLE_LOADING); - }); - - test('it configures data and subtitle', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); - expect( - wrapper.find('[dataTestSubj="queryPreviewCustomHistogram"]').at(0).prop('subtitle') - ).toEqual(i18n.QUERY_PREVIEW_TITLE(9154)); - expect(wrapper.find('[dataTestSubj="queryPreviewCustomHistogram"]').at(0).props().data).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, - }, - ], - }, - ] - ); - }); - - test('it invokes setQuery with id, inspect, isLoading and refetch', async () => { - const mockRefetch = jest.fn(); - - mount( - - - - ); - - expect(mockSetQuery).toHaveBeenCalledWith({ - id: 'queryPreviewCustomHistogramQuery', - inspect: { dsl: ['some dsl'], response: ['query response'] }, - loading: false, - refetch: mockRefetch, - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.tsx deleted file mode 100644 index 5392b088891289..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.tsx +++ /dev/null @@ -1,76 +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, { useEffect, useMemo } from 'react'; - -import * as i18n from '../rule_preview/translations'; -import { useGlobalTime } from '../../../../common/containers/use_global_time'; -import { getHistogramConfig } from '../rule_preview/helpers'; -import { - ChartSeriesConfigs, - ChartSeriesData, - ChartData, -} from '../../../../common/components/charts/common'; -import { InspectResponse } from '../../../../../public/types'; -import { inputsModel } from '../../../../common/store'; -import { PreviewHistogram } from './histogram'; - -export const ID = 'queryPreviewCustomHistogramQuery'; - -interface PreviewCustomQueryHistogramProps { - to: string; - from: string; - isLoading: boolean; - data: ChartData[]; - totalCount: number; - inspect: InspectResponse; - refetch: inputsModel.Refetch; -} - -export const PreviewCustomQueryHistogram = ({ - to, - from, - data, - totalCount, - inspect, - refetch, - isLoading, -}: PreviewCustomQueryHistogramProps) => { - const { setQuery, isInitializing } = useGlobalTime(); - - useEffect((): void => { - if (!isLoading && !isInitializing) { - setQuery({ id: ID, inspect, loading: isLoading, refetch }); - } - }, [setQuery, inspect, isLoading, isInitializing, refetch]); - - const barConfig = useMemo( - (): ChartSeriesConfigs => getHistogramConfig(to, from, true), - [from, to] - ); - - const subtitle = useMemo( - (): string => - isLoading ? i18n.QUERY_PREVIEW_SUBTITLE_LOADING : i18n.QUERY_PREVIEW_TITLE(totalCount), - [isLoading, totalCount] - ); - - const chartData = useMemo((): ChartSeriesData[] => [{ key: 'hits', value: data }], [data]); - - return ( - - ); -}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx deleted file mode 100644 index df32223fc7ec38..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx +++ /dev/null @@ -1,152 +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 { mount } from 'enzyme'; - -import * as i18n from '../rule_preview/translations'; -import { useGlobalTime } from '../../../../common/containers/use_global_time'; -import { TestProviders } from '../../../../common/mock'; -import { PreviewEqlQueryHistogram } from './eql_histogram'; - -jest.mock('../../../../common/containers/use_global_time'); - -describe('PreviewEqlQueryHistogram', () => { - const mockSetQuery = jest.fn(); - - beforeEach(() => { - (useGlobalTime as jest.Mock).mockReturnValue({ - from: '2020-07-07T08:20:18.966Z', - isInitializing: false, - to: '2020-07-08T08:20:18.966Z', - setQuery: mockSetQuery, - }); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - test('it renders loader when isLoading is true', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy(); - expect( - wrapper.find('[dataTestSubj="queryPreviewEqlHistogram"]').at(0).prop('subtitle') - ).toEqual(i18n.QUERY_PREVIEW_SUBTITLE_LOADING); - }); - - test('it configures data and subtitle', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); - expect( - wrapper.find('[dataTestSubj="queryPreviewEqlHistogram"]').at(0).prop('subtitle') - ).toEqual(i18n.QUERY_PREVIEW_TITLE(9154)); - expect(wrapper.find('[dataTestSubj="queryPreviewEqlHistogram"]').at(0).props().data).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, - }, - ], - }, - ]); - }); - - test('it invokes setQuery with id, inspect, isLoading and refetch', async () => { - const mockRefetch = jest.fn(); - - mount( - - - - ); - - expect(mockSetQuery).toHaveBeenCalledWith({ - id: 'queryEqlPreviewHistogramQuery', - inspect: { dsl: ['some dsl'], response: ['query response'] }, - loading: false, - refetch: mockRefetch, - }); - }); - - test('it displays histogram', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); - expect( - wrapper.find('[data-test-subj="sharedPreviewQueryNoHistogramAvailable"]').exists() - ).toBeFalsy(); - expect(wrapper.find('[data-test-subj="sharedPreviewQueryHistogram"]').exists()).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.tsx deleted file mode 100644 index eae2a593d5f258..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.tsx +++ /dev/null @@ -1,73 +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, { useEffect, useMemo } from 'react'; - -import * as i18n from '../rule_preview/translations'; -import { getHistogramConfig } from '../rule_preview/helpers'; -import { - ChartSeriesData, - ChartSeriesConfigs, - ChartData, -} from '../../../../common/components/charts/common'; -import { InspectQuery } from '../../../../common/store/inputs/model'; -import { useGlobalTime } from '../../../../common/containers/use_global_time'; -import { inputsModel } from '../../../../common/store'; -import { PreviewHistogram } from './histogram'; - -export const ID = 'queryEqlPreviewHistogramQuery'; - -interface PreviewEqlQueryHistogramProps { - to: string; - from: string; - totalCount: number; - isLoading: boolean; - data: ChartData[]; - inspect: InspectQuery; - refetch: inputsModel.Refetch; -} - -export const PreviewEqlQueryHistogram = ({ - from, - to, - totalCount, - data, - inspect, - refetch, - isLoading, -}: PreviewEqlQueryHistogramProps) => { - const { setQuery, isInitializing } = useGlobalTime(); - - useEffect((): void => { - if (!isInitializing) { - setQuery({ id: ID, inspect, loading: false, refetch }); - } - }, [setQuery, inspect, isInitializing, refetch]); - - const barConfig = useMemo((): ChartSeriesConfigs => getHistogramConfig(to, from), [from, to]); - - const subtitle = useMemo( - (): string => - isLoading ? i18n.QUERY_PREVIEW_SUBTITLE_LOADING : i18n.QUERY_PREVIEW_TITLE(totalCount), - [isLoading, totalCount] - ); - - const chartData = useMemo((): ChartSeriesData[] => [{ key: 'hits', value: data }], [data]); - - return ( - - ); -}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.test.tsx deleted file mode 100644 index 500a7f3d0e3dbb..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.test.tsx +++ /dev/null @@ -1,74 +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 { mount } from 'enzyme'; - -import { TestProviders } from '../../../../common/mock'; -import { PreviewHistogram } from './histogram'; -import { getHistogramConfig } from '../rule_preview/helpers'; - -describe('PreviewHistogram', () => { - test('it renders loading icon if "isLoading" is true', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="sharedPreviewQueryHistogram"]').exists()).toBeFalsy(); - }); - - test('it renders chart if "isLoading" is true', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="sharedPreviewQueryHistogram"]').exists()).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.tsx deleted file mode 100644 index 3391ed1c5560a9..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.tsx +++ /dev/null @@ -1,75 +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 { EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer, EuiLoadingChart } from '@elastic/eui'; -import styled from 'styled-components'; - -import { BarChart } from '../../../../common/components/charts/barchart'; -import { Panel } from '../../../../common/components/panel'; -import { HeaderSection } from '../../../../common/components/header_section'; -import { ChartSeriesData, ChartSeriesConfigs } from '../../../../common/components/charts/common'; - -const LoadingChart = styled(EuiLoadingChart)` - display: block; - margin: 0 auto; -`; - -interface PreviewHistogramProps { - id: string; - data: ChartSeriesData[]; - dataTestSubj?: string; - barConfig: ChartSeriesConfigs; - title: string; - subtitle: string; - disclaimer: string; - isLoading: boolean; -} - -export const PreviewHistogram = ({ - id, - data, - dataTestSubj, - barConfig, - title, - subtitle, - disclaimer, - isLoading, -}: PreviewHistogramProps) => { - return ( - <> - - - - - - - {isLoading ? ( - - ) : ( - - )} - - - <> - - -

{disclaimer}

-
- -
-
-
- - ); -}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx deleted file mode 100644 index f14bd5f7354d92..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx +++ /dev/null @@ -1,502 +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 { of } from 'rxjs'; -import { ThemeProvider } from 'styled-components'; -import { mount } from 'enzyme'; - -import { TestProviders } from '../../../../common/mock'; -import { useKibana } from '../../../../common/lib/kibana'; -import { PreviewQuery } from './'; -import { getMockEqlResponse } from '../../../../common/hooks/eql/eql_search_response.mock'; -import { useMatrixHistogram } from '../../../../common/containers/matrix_histogram'; -import { useEqlPreview } from '../../../../common/hooks/eql/'; -import { getMockTheme } from '../../../../common/lib/kibana/kibana_react.mock'; -import type { FilterMeta } from '@kbn/es-query'; - -const mockTheme = getMockTheme({ - eui: { - euiSuperDatePickerWidth: '180px', - }, -}); - -jest.mock('../../../../common/lib/kibana'); -jest.mock('../../../../common/containers/matrix_histogram'); -jest.mock('../../../../common/hooks/eql/'); - -describe('PreviewQuery', () => { - beforeEach(() => { - useKibana().services.notifications.toasts.addError = jest.fn(); - - useKibana().services.notifications.toasts.addWarning = jest.fn(); - - (useMatrixHistogram as jest.Mock).mockReturnValue([ - false, - { - inspect: { dsl: [], response: [] }, - totalCount: 1, - refetch: jest.fn(), - data: [], - buckets: [], - }, - (useKibana().services.data.search.search as jest.Mock).mockReturnValue( - of(getMockEqlResponse()) - ), - ]); - - (useEqlPreview as jest.Mock).mockReturnValue([ - false, - (useKibana().services.data.search.search as jest.Mock).mockReturnValue( - of(getMockEqlResponse()) - ), - { - inspect: { dsl: [], response: [] }, - totalCount: 1, - refetch: jest.fn(), - data: [], - buckets: [], - }, - ]); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - test('it renders timeframe select and preview button on render', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="queryPreviewSelect"]').exists()).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="queryPreviewButton"] button').props().disabled - ).toBeFalsy(); - expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy(); - }); - - test('it renders preview button disabled if "isDisabled" is true', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find('[data-test-subj="queryPreviewButton"] button').props().disabled - ).toBeTruthy(); - }); - - test('it renders preview button disabled if "query" is undefined', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find('[data-test-subj="queryPreviewButton"] button').props().disabled - ).toBeTruthy(); - }); - - test('it renders preview button enabled if query exists', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find('[data-test-subj="queryPreviewButton"] button').props().disabled - ).toBeFalsy(); - }); - - test('it renders preview button enabled if no query exists but filters do exist', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find('[data-test-subj="queryPreviewButton"] button').props().disabled - ).toBeFalsy(); - }); - - test('it renders query histogram when rule type is query and preview button clicked', () => { - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; - - expect(mockCalls.length).toEqual(1); - expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy(); - }); - - test('it renders noise warning when rule type is query, timeframe is last hour and hit average is greater than 1/hour', async () => { - const wrapper = mount( - - - - ); - - (useMatrixHistogram as jest.Mock).mockReturnValue([ - false, - { - inspect: { dsl: [], response: [] }, - totalCount: 2, - refetch: jest.fn(), - data: [], - buckets: [], - }, - (useKibana().services.data.search.search as jest.Mock).mockReturnValue( - of(getMockEqlResponse()) - ), - ]); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeTruthy(); - }); - - test('it renders query histogram when rule type is saved_query and preview button clicked', () => { - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; - - expect(mockCalls.length).toEqual(1); - expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy(); - }); - - test('it renders eql histogram when preview button clicked and rule type is eql', () => { - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; - - expect(mockCalls.length).toEqual(1); - expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeTruthy(); - }); - - test('it renders noise warning when rule type is eql, timeframe is last hour and hit average is greater than 1/hour', async () => { - const wrapper = mount( - - - - ); - - (useEqlPreview as jest.Mock).mockReturnValue([ - false, - (useKibana().services.data.search.search as jest.Mock).mockReturnValue( - of(getMockEqlResponse()) - ), - { - inspect: { dsl: [], response: [] }, - totalCount: 2, - refetch: jest.fn(), - data: [], - buckets: [], - }, - ]); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeTruthy(); - }); - - test('it renders threshold histogram when preview button clicked, rule type is threshold, and threshold field is defined', () => { - const wrapper = mount( - - - - ); - - (useMatrixHistogram as jest.Mock).mockReturnValue([ - false, - { - inspect: { dsl: [], response: [] }, - totalCount: 500, - refetch: jest.fn(), - data: [], - buckets: [{ key: 'siem-kibana', doc_count: 500 }], - }, - (useKibana().services.data.search.search as jest.Mock).mockReturnValue( - of(getMockEqlResponse()) - ), - ]); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; - - expect(mockCalls.length).toEqual(1); - expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy(); - }); - - test('it renders noise warning when rule type is threshold, and threshold field is defined, timeframe is last hour and hit average is greater than 1/hour', async () => { - const wrapper = mount( - - - - ); - - (useMatrixHistogram as jest.Mock).mockReturnValue([ - false, - { - inspect: { dsl: [], response: [] }, - totalCount: 500, - refetch: jest.fn(), - data: [], - buckets: [ - { key: 'siem-kibana', doc_count: 200 }, - { key: 'siem-windows', doc_count: 300 }, - ], - }, - (useKibana().services.data.search.search as jest.Mock).mockReturnValue( - of(getMockEqlResponse()) - ), - ]); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeTruthy(); - }); - - test('it renders query histogram when preview button clicked, rule type is threshold, and threshold field is empty array', () => { - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; - - expect(mockCalls.length).toEqual(1); - expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy(); - }); - - test('it renders query histogram when preview button clicked, rule type is threshold, and threshold field is empty string', () => { - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; - - expect(mockCalls.length).toEqual(1); - expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy(); - }); - - test('it hides histogram when timeframe changes', () => { - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeTruthy(); - - wrapper - .find('[data-test-subj="queryPreviewTimeframeSelect"] select') - .at(0) - .simulate('change', { target: { value: 'd' } }); - - expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeFalsy(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx deleted file mode 100644 index e7cc34ef49beff..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx +++ /dev/null @@ -1,362 +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, { Fragment, useCallback, useEffect, useMemo, useReducer, useRef } from 'react'; -import { Unit } from '@elastic/datemath'; -import styled from 'styled-components'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiSelect, - EuiFormRow, - EuiButton, - EuiCallOut, - EuiText, - EuiSpacer, -} from '@elastic/eui'; -import { debounce } from 'lodash/fp'; - -import { Type } from '@kbn/securitysolution-io-ts-alerting-types'; -import * as i18n from '../rule_preview/translations'; -import { useMatrixHistogram } from '../../../../common/containers/matrix_histogram'; -import { MatrixHistogramType } from '../../../../../common/search_strategy'; -import { FieldValueQueryBar } from '../query_bar'; -import { PreviewEqlQueryHistogram } from './eql_histogram'; -import { useEqlPreview } from '../../../../common/hooks/eql/'; -import { PreviewThresholdQueryHistogram } from './threshold_histogram'; -import { formatDate } from '../../../../common/components/super_date_picker'; -import { State, queryPreviewReducer } from './reducer'; -import { isNoisy } from '../rule_preview/helpers'; -import { PreviewCustomQueryHistogram } from './custom_histogram'; -import { FieldValueThreshold } from '../threshold_input'; - -const Select = styled(EuiSelect)` - width: ${({ theme }) => theme.eui.euiSuperDatePickerWidth}; -`; - -const PreviewButton = styled(EuiButton)` - margin-left: 0; -`; - -export const initialState: State = { - timeframeOptions: [], - showHistogram: false, - timeframe: 'h', - warnings: [], - queryFilter: undefined, - toTime: '', - fromTime: '', - queryString: '', - language: 'kuery', - filters: [], - thresholdFieldExists: false, - showNonEqlHistogram: false, -}; - -export type Threshold = FieldValueThreshold | undefined; - -interface PreviewQueryProps { - dataTestSubj: string; - idAria: string; - query: FieldValueQueryBar | undefined; - index: string[]; - ruleType: Type; - threshold: Threshold; - isDisabled: boolean; -} - -export const PreviewQuery = ({ - ruleType, - dataTestSubj, - idAria, - query, - index, - threshold, - isDisabled, -}: PreviewQueryProps) => { - const [ - eqlQueryLoading, - startEql, - { - totalCount: eqlQueryTotal, - data: eqlQueryData, - refetch: eqlQueryRefetch, - inspect: eqlQueryInspect, - }, - ] = useEqlPreview(); - - const [ - { - thresholdFieldExists, - showNonEqlHistogram, - timeframeOptions, - showHistogram, - timeframe, - warnings, - queryFilter, - toTime, - fromTime, - queryString, - }, - dispatch, - ] = useReducer(queryPreviewReducer(), { - ...initialState, - toTime: formatDate('now-1h'), - fromTime: formatDate('now'), - }); - const [ - isMatrixHistogramLoading, - { inspect, totalCount: matrixHistTotal, refetch, data: matrixHistoData, buckets }, - startNonEql, - ] = useMatrixHistogram({ - errorMessage: i18n.QUERY_PREVIEW_ERROR, - endDate: fromTime, - startDate: toTime, - filterQuery: queryFilter, - indexNames: index, - includeMissingData: false, - histogramType: MatrixHistogramType.events, - stackByField: 'event.category', - threshold: ruleType === 'threshold' ? threshold : undefined, - skip: true, - }); - - const setQueryInfo = useCallback( - (queryBar: FieldValueQueryBar | undefined, indices: string[], type: Type): void => { - dispatch({ - type: 'setQueryInfo', - queryBar, - index: indices, - ruleType: type, - }); - }, - [dispatch] - ); - - const debouncedSetQueryInfo = useRef(debounce(500, setQueryInfo)); - - const setTimeframeSelect = useCallback( - (selection: Unit): void => { - dispatch({ - type: 'setTimeframeSelect', - timeframe: selection, - }); - }, - [dispatch] - ); - - const setRuleTypeChange = useCallback( - (type: Type): void => { - dispatch({ - type: 'setResetRuleTypeChange', - ruleType: type, - }); - }, - [dispatch] - ); - - const setWarnings = useCallback( - (yikes: string[]): void => { - dispatch({ - type: 'setWarnings', - warnings: yikes, - }); - }, - [dispatch] - ); - - const setNoiseWarning = useCallback((): void => { - dispatch({ - type: 'setNoiseWarning', - }); - }, [dispatch]); - - const setShowHistogram = useCallback( - (show: boolean): void => { - dispatch({ - type: 'setShowHistogram', - show, - }); - }, - [dispatch] - ); - - const setThresholdValues = useCallback( - (thresh: Threshold, type: Type): void => { - dispatch({ - type: 'setThresholdQueryVals', - threshold: thresh, - ruleType: type, - }); - }, - [dispatch] - ); - - useEffect(() => { - debouncedSetQueryInfo.current(query, index, ruleType); - }, [index, query, ruleType]); - - useEffect((): void => { - setThresholdValues(threshold, ruleType); - }, [setThresholdValues, threshold, ruleType]); - - useEffect((): void => { - setRuleTypeChange(ruleType); - }, [ruleType, setRuleTypeChange]); - - useEffect((): void => { - switch (ruleType) { - case 'eql': - if (isNoisy(eqlQueryTotal, timeframe)) { - setNoiseWarning(); - } - break; - case 'threshold': - const totalHits = thresholdFieldExists ? buckets.length : matrixHistTotal; - if (isNoisy(totalHits, timeframe)) { - setNoiseWarning(); - } - break; - default: - if (isNoisy(matrixHistTotal, timeframe)) { - setNoiseWarning(); - } - } - }, [ - timeframe, - matrixHistTotal, - eqlQueryTotal, - ruleType, - setNoiseWarning, - thresholdFieldExists, - buckets.length, - ]); - - const handlePreviewEqlQuery = useCallback( - (to: string, from: string): void => { - startEql({ - index, - query: queryString, - from, - to, - interval: timeframe, - }); - }, - [startEql, index, queryString, timeframe] - ); - - const handleSelectPreviewTimeframe = useCallback( - ({ target: { value } }: React.ChangeEvent): void => { - setTimeframeSelect(value as Unit); - }, - [setTimeframeSelect] - ); - - const handlePreviewClicked = useCallback((): void => { - const to = formatDate('now'); - const from = formatDate(`now-1${timeframe}`); - - setWarnings([]); - setShowHistogram(true); - - if (ruleType === 'eql') { - handlePreviewEqlQuery(to, from); - } else { - startNonEql(to, from); - } - }, [setWarnings, setShowHistogram, ruleType, handlePreviewEqlQuery, startNonEql, timeframe]); - - const previewButtonDisabled = useMemo(() => { - return ( - isMatrixHistogramLoading || - eqlQueryLoading || - isDisabled || - query == null || - (query != null && query.query.query === '' && query.filters.length === 0) - ); - }, [eqlQueryLoading, isDisabled, isMatrixHistogramLoading, query]); - - return ( - <> - - - -