From c2985c4daa66f326dce01f2a79eb7e8da470c72e Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Mon, 25 Jul 2022 14:42:05 -0400 Subject: [PATCH] [Security Solution] [Platform] Adds state to remember what was in data view or index pattern selection when switching between the two (#136448) Co-authored-by: Khristinin Nikita --- .../rules/data_view_selector/index.tsx | 39 ++---- .../rules/eql_query_bar/validators.ts | 7 +- .../rules/step_about_rule/index.test.tsx | 2 + .../rules/step_define_rule/index.tsx | 122 ++++++++++++------ .../rules/step_define_rule/schema.tsx | 9 +- .../rules/all/__mocks__/mock.ts | 2 + .../detection_engine/rules/create/helpers.ts | 29 ++++- .../detection_engine/rules/helpers.test.tsx | 6 + .../pages/detection_engine/rules/helpers.tsx | 2 + .../pages/detection_engine/rules/types.ts | 6 + 10 files changed, 157 insertions(+), 67 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/data_view_selector/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/data_view_selector/index.tsx index ec19332ee3d37..d319d3f24f547 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/data_view_selector/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/data_view_selector/index.tsx @@ -11,26 +11,17 @@ import type { EuiComboBoxOptionOption } from '@elastic/eui'; import { EuiCallOut, EuiComboBox, EuiFormRow, EuiSpacer } from '@elastic/eui'; import type { DataViewListItem } from '@kbn/data-views-plugin/common'; -import type { DataViewBase } from '@kbn/es-query'; import type { FieldHook } from '../../../../shared_imports'; import { getFieldValidityAndErrorMessage } from '../../../../shared_imports'; import * as i18n from './translations'; -import { useKibana } from '../../../../common/lib/kibana'; import type { DefineStepRule } from '../../../pages/detection_engine/rules/types'; interface DataViewSelectorProps { - kibanaDataViews: { [x: string]: DataViewListItem }; + kibanaDataViews: Record; field: FieldHook; - setIndexPattern: (indexPattern: DataViewBase) => void; } -export const DataViewSelector = ({ - kibanaDataViews, - field, - setIndexPattern, -}: DataViewSelectorProps) => { - const { data } = useKibana().services; - +export const DataViewSelector = ({ kibanaDataViews, field }: DataViewSelectorProps) => { let isInvalid; let errorMessage; let dataViewId: string | null | undefined; @@ -62,7 +53,15 @@ export const DataViewSelector = ({ : [] ); - const [selectedDataView, setSelectedDataView] = useState(); + useEffect(() => { + if (!selectedDataViewNotFound && dataViewId) { + setSelectedOption([ + { id: kibanaDataViews[dataViewId].id, label: kibanaDataViews[dataViewId].title }, + ]); + } else { + setSelectedOption([]); + } + }, [dataViewId, field, kibanaDataViews, selectedDataViewNotFound]); // TODO: optimize this, pass down array of data view ids // at the same time we grab the data views in the top level form component @@ -75,17 +74,6 @@ export const DataViewSelector = ({ : []; }, [kibanaDataViewsDefined, kibanaDataViews]); - useEffect(() => { - const fetchSingleDataView = async () => { - if (selectedDataView != null) { - const dv = await data.dataViews.get(selectedDataView.id); - setIndexPattern(dv); - } - }; - - fetchSingleDataView(); - }, [data.dataViews, selectedDataView, setIndexPattern]); - const onChangeDataViews = (options: Array>) => { const selectedDataViewOption = options; @@ -96,10 +84,9 @@ export const DataViewSelector = ({ selectedDataViewOption.length > 0 && selectedDataViewOption[0].id != null ) { - setSelectedDataView(kibanaDataViews[selectedDataViewOption[0].id]); - field?.setValue(selectedDataViewOption[0].id); + const selectedDataViewId = selectedDataViewOption[0].id; + field?.setValue(selectedDataViewId); } else { - setSelectedDataView(undefined); field?.setValue(undefined); } }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/validators.ts b/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/validators.ts index 8ca8355bc6502..ae6e84f187109 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/validators.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/validators.ts @@ -11,6 +11,7 @@ import type { FieldHook, ValidationError, ValidationFunc } from '../../../../sha import { isEqlRule } from '../../../../../common/detection_engine/utils'; import { KibanaServices } from '../../../../common/lib/kibana'; import type { DefineStepRule } from '../../../pages/detection_engine/rules/types'; +import { DataSourceType } from '../../../pages/detection_engine/rules/types'; import { validateEql } from '../../../../common/hooks/eql/api'; import type { FieldValueQueryBar } from '../query_bar'; import * as i18n from './translations'; @@ -69,7 +70,11 @@ export const eqlValidator = async ( const { data } = KibanaServices.get(); let dataViewTitle = index?.join(); let runtimeMappings = {}; - if (dataViewId != null) { + if ( + dataViewId != null && + dataViewId !== '' && + formData.dataSourceType === DataSourceType.DataView + ) { const dataView = await data.dataViews.get(dataViewId); dataViewTitle = dataView.title; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx index 1b92ac0667a7c..6391d785ef39d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx @@ -22,6 +22,7 @@ import type { RuleStep, DefineStepRule, } from '../../../pages/detection_engine/rules/types'; +import { DataSourceType } from '../../../pages/detection_engine/rules/types'; import { fillEmptySeverityMappings } from '../../../pages/detection_engine/rules/helpers'; import { TestProviders } from '../../../../common/mock'; @@ -54,6 +55,7 @@ export const stepDefineStepMLRule: DefineStepRule = { threatMapping: [], timeline: { id: null, title: null }, eqlOptions: {}, + dataSourceType: DataSourceType.IndexPatterns, newTermsFields: ['host.ip'], historyWindowSize: '7d', }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx index c63442939905b..de2ad79d44cb5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx @@ -36,11 +36,14 @@ import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { hasMlAdminPermissions } from '../../../../../common/machine_learning/has_ml_admin_permissions'; import { hasMlLicense } from '../../../../../common/machine_learning/has_ml_license'; import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml_capabilities'; -import { useUiSetting$ } from '../../../../common/lib/kibana'; +import { useUiSetting$, useKibana } from '../../../../common/lib/kibana'; import type { EqlOptionsSelected, FieldsEqlOptions } from '../../../../../common/search_strategy'; -import { filterRuleFieldsForType } from '../../../pages/detection_engine/rules/create/helpers'; +import { + filterRuleFieldsForType, + getStepDataDataSource, +} from '../../../pages/detection_engine/rules/create/helpers'; import type { DefineStepRule, RuleStepProps } from '../../../pages/detection_engine/rules/types'; -import { RuleStep } from '../../../pages/detection_engine/rules/types'; +import { RuleStep, DataSourceType } from '../../../pages/detection_engine/rules/types'; import { StepRuleDescription } from '../description_step'; import { QueryBarDefineRule } from '../query_bar'; import { SelectRuleType } from '../select_rule_type'; @@ -78,11 +81,11 @@ import { NewTermsFields } from '../new_terms_fields'; import { ScheduleItem } from '../schedule_item_form'; import { DocLink } from '../../../../common/components/links_to_docs/doc_link'; -const DATA_VIEW_SELECT_ID = 'dataView'; -const INDEX_PATTERN_SELECT_ID = 'indexPatterns'; - const CommonUseField = getUseField({ component: Field }); +const StyledVisibleContainer = styled.div<{ isVisible: boolean }>` + display: ${(props) => (props.isVisible ? 'block' : 'none')}; +`; interface StepDefineRuleProps extends RuleStepProps { defaultValues?: DefineStepRule; } @@ -119,6 +122,7 @@ export const stepDefineDefaultValue: DefineStepRule = { title: DEFAULT_TIMELINE_TITLE, }, eqlOptions: {}, + dataSourceType: DataSourceType.IndexPatterns, newTermsFields: [], historyWindowSize: '7d', }; @@ -174,6 +178,7 @@ const StepDefineRuleComponent: FC = ({ const [openTimelineSearch, setOpenTimelineSearch] = useState(false); const [indexModified, setIndexModified] = useState(false); const [threatIndexModified, setThreatIndexModified] = useState(false); + const [dataViewTitle, setDataViewTitle] = useState(); const [indicesConfig] = useUiSetting$(DEFAULT_INDEX_KEY); const [threatIndicesConfig] = useUiSetting$(DEFAULT_THREAT_INDEX_KEY); @@ -202,6 +207,7 @@ const StepDefineRuleComponent: FC = ({ threatMapping: formThreatMapping, machineLearningJobId: formMachineLearningJobId, anomalyThreshold: formAnomalyThreshold, + dataSourceType: formDataSourceType, newTermsFields: formNewTermsFields, historyWindowSize: formHistoryWindowSize, }, @@ -221,6 +227,7 @@ const StepDefineRuleComponent: FC = ({ 'threatMapping', 'machineLearningJobId', 'anomalyThreshold', + 'dataSourceType', 'newTermsFields', 'historyWindowSize', ], @@ -236,6 +243,7 @@ const StepDefineRuleComponent: FC = ({ const newTermsFields = formNewTermsFields ?? initialState.newTermsFields; const historyWindowSize = formHistoryWindowSize ?? initialState.historyWindowSize; const ruleType = formRuleType || initialState.ruleType; + const dataSourceType = formDataSourceType || initialState.dataSourceType; // if 'index' is selected, use these browser fields // otherwise use the dataview browserfields @@ -243,24 +251,51 @@ const StepDefineRuleComponent: FC = ({ const [optionsSelected, setOptionsSelected] = useState( defaultValues?.eqlOptions || {} ); - const [initIsIndexPatternLoading, { browserFields, indexPatterns: initIndexPattern }] = - useFetchIndex(index, false); - const [indexPattern, setIndexPattern] = useState(initIndexPattern); - const [isIndexPatternLoading, setIsIndexPatternLoading] = useState(initIsIndexPatternLoading); - const [dataSourceRadioIdSelected, setDataSourceRadioIdSelected] = useState( - dataView == null || dataView === '' ? INDEX_PATTERN_SELECT_ID : DATA_VIEW_SELECT_ID + const [isIndexPatternLoading, { browserFields, indexPatterns: initIndexPattern }] = useFetchIndex( + index, + false ); + const [indexPattern, setIndexPattern] = useState(initIndexPattern); + const { data } = useKibana().services; + + // Why do we need this? to ensure the query bar auto-suggest gets the latest updates + // when the index pattern changes + // when we select new dataView + // when we choose some other dataSourceType useEffect(() => { - if (dataSourceRadioIdSelected === INDEX_PATTERN_SELECT_ID) { - setIndexPattern(initIndexPattern); + if (dataSourceType === DataSourceType.IndexPatterns) { + if (!isIndexPatternLoading) { + setIndexPattern(initIndexPattern); + } } - }, [initIndexPattern, dataSourceRadioIdSelected]); + + if (dataSourceType === DataSourceType.DataView) { + const fetchDataView = async () => { + if (dataView != null) { + const dv = await data.dataViews.get(dataView); + setDataViewTitle(dv.title); + setIndexPattern(dv); + } + }; + + fetchDataView(); + } + }, [dataSourceType, isIndexPatternLoading, data, dataView, initIndexPattern]); // Callback for when user toggles between Data Views and Index Patterns - const onChangeDataSource = (optionId: string) => { - setDataSourceRadioIdSelected(optionId); - }; + const onChangeDataSource = useCallback( + (optionId: string) => { + form.setFieldValue('dataSourceType', optionId); + form.getFields().index.reset({ + resetValue: false, + }); + form.getFields().dataViewId.reset({ + resetValue: false, + }); + }, + [form] + ); const [aggFields, setAggregatableFields] = useState([]); @@ -433,28 +468,26 @@ const StepDefineRuleComponent: FC = ({ const dataViewIndexPatternToggleButtonOptions: EuiButtonGroupOptionProps[] = useMemo( () => [ { - id: INDEX_PATTERN_SELECT_ID, + id: DataSourceType.IndexPatterns, label: i18nCore.translate( 'xpack.securitySolution.ruleDefine.indexTypeSelect.indexPattern', { defaultMessage: 'Index Patterns', } ), - iconType: - dataSourceRadioIdSelected === INDEX_PATTERN_SELECT_ID ? 'checkInCircleFilled' : 'empty', - 'data-test-subj': `rule-index-toggle-${INDEX_PATTERN_SELECT_ID}`, + iconType: dataSourceType === DataSourceType.IndexPatterns ? 'checkInCircleFilled' : 'empty', + 'data-test-subj': `rule-index-toggle-${DataSourceType.IndexPatterns}`, }, { - id: DATA_VIEW_SELECT_ID, + id: DataSourceType.DataView, label: i18nCore.translate('xpack.securitySolution.ruleDefine.indexTypeSelect.dataView', { defaultMessage: 'Data View', }), - iconType: - dataSourceRadioIdSelected === DATA_VIEW_SELECT_ID ? 'checkInCircleFilled' : 'empty', - 'data-test-subj': `rule-index-toggle-${DATA_VIEW_SELECT_ID}`, + iconType: dataSourceType === DataSourceType.DataView ? 'checkInCircleFilled' : 'empty', + 'data-test-subj': `rule-index-toggle-${DataSourceType.DataView}`, }, ], - [dataSourceRadioIdSelected] + [dataSourceType] ); const DataViewSelectorMemo = useMemo(() => { @@ -465,8 +498,6 @@ const StepDefineRuleComponent: FC = ({ component={DataViewSelector} componentProps={{ kibanaDataViews, - setIndexPattern, - setIsIndexPatternLoading, }} /> ); @@ -503,7 +534,7 @@ const StepDefineRuleComponent: FC = ({ isFullWidth={true} legend="Rule index pattern or data view selector" data-test-subj="dataViewIndexPatternButtonGroup" - idSelected={dataSourceRadioIdSelected} + idSelected={dataSourceType} onChange={onChangeDataSource} options={dataViewIndexPatternToggleButtonOptions} color="primary" @@ -512,9 +543,10 @@ const StepDefineRuleComponent: FC = ({ - {dataSourceRadioIdSelected === DATA_VIEW_SELECT_ID ? ( - DataViewSelectorMemo - ) : ( + + {DataViewSelectorMemo} + + = ({ }, }} /> - )} + ); }, [ - dataSourceRadioIdSelected, + dataSourceType, dataViewIndexPatternToggleButtonOptions, DataViewSelectorMemo, indexModified, handleResetIndices, + onChangeDataSource, ]); const QueryBarMemo = useMemo( @@ -619,19 +652,36 @@ const StepDefineRuleComponent: FC = ({ [indexPattern] ); + const dataForDescription: Partial = getStepDataDataSource(initialState); + + if (dataSourceType === DataSourceType.DataView) { + dataForDescription.dataViewTitle = dataViewTitle; + } + return isReadOnlyView ? ( ) : ( <>
+ + + = { ...args: Parameters ): ReturnType> | undefined => { const [{ formData }] = args; - const skipValidation = isMlRule(formData.ruleType) || formData.dataViewId != null; + const skipValidation = + isMlRule(formData.ruleType) || formData.dataSourceType !== DataSourceType.IndexPatterns; if (skipValidation) { return; @@ -94,10 +96,11 @@ export const schema: FormSchema = { // the dropdown defaults the dataViewId to an empty string somehow on render.. // need to figure this out. const notEmptyDataViewId = formData.dataViewId != null && formData.dataViewId !== ''; + const skipValidation = isMlRule(formData.ruleType) || - ((formData.index != null || notEmptyDataViewId) && - !(formData.index != null && notEmptyDataViewId)); + notEmptyDataViewId || + formData.dataSourceType !== DataSourceType.DataView; if (skipValidation) { return; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts index 243b5195788b3..fdf1370587638 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts @@ -8,6 +8,7 @@ import { FilterStateStore } from '@kbn/es-query'; import type { Rule } from '../../../../../containers/detection_engine/rules'; import type { AboutStepRule, ActionsStepRule, DefineStepRule, ScheduleStepRule } from '../../types'; +import { DataSourceType } from '../../types'; import type { FieldValueQueryBar } from '../../../../../components/rules/query_bar'; import { fillEmptySeverityMappings } from '../../helpers'; import { getThreatMock } from '../../../../../../../common/detection_engine/schemas/types/threat.mock'; @@ -216,6 +217,7 @@ export const mockDefineStepRule = (): DefineStepRule => ({ }, }, eqlOptions: {}, + dataSourceType: DataSourceType.IndexPatterns, newTermsFields: ['host.ip'], historyWindowSize: '7d', }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts index a3da3f49587d3..e792a98b15459 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts @@ -9,6 +9,7 @@ import { has, isEmpty } from 'lodash/fp'; import type { Unit } from '@kbn/datemath'; import moment from 'moment'; import deepmerge from 'deepmerge'; +import omit from 'lodash/omit'; import type { ExceptionListType, @@ -39,6 +40,7 @@ import type { RuleStepsFormData, RuleStep, } from '../types'; +import { DataSourceType } from '../types'; import type { FieldValueQueryBar } from '../../../../components/rules/query_bar'; import type { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request'; import { stepDefineDefaultValue } from '../../../../components/rules/step_define_rule'; @@ -336,9 +338,34 @@ export const filterEmptyThreats = (threats: Threats): Threats => { }); }; +/** + * remove unused data source. + * Ex: rule is using a data view so we should not + * write an index property on the rule form. + * @param defineStepData + * @returns DefineStepRule + */ +export const getStepDataDataSource = ( + defineStepData: DefineStepRule +): Omit & { + index?: string[]; + dataViewId?: string; +} => { + const copiedStepData = { ...defineStepData }; + if (defineStepData.dataSourceType === DataSourceType.DataView) { + return omit(copiedStepData, ['index', 'dataSourceType']); + } else if (defineStepData.dataSourceType === DataSourceType.IndexPatterns) { + return omit(copiedStepData, ['dataViewId', 'dataSourceType']); + } + return copiedStepData; +}; + export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStepRuleJson => { - const ruleFields = filterRuleFieldsForType(defineStepData, defineStepData.ruleType); + const stepData = getStepDataDataSource(defineStepData); + + const ruleFields = filterRuleFieldsForType(stepData, stepData.ruleType); const { ruleType, timeline } = ruleFields; + const baseFields = { type: ruleType, ...(timeline.id != null && diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx index be2cce0b1486c..fd1e8059d047b 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx @@ -50,6 +50,8 @@ describe('rule helpers', () => { const defineRuleStepData = { ruleType: 'saved_query', anomalyThreshold: 50, + dataSourceType: 'indexPatterns', + dataViewId: undefined, index: ['auditbeat-*'], machineLearningJobId: [], queryBar: { @@ -215,6 +217,8 @@ describe('rule helpers', () => { const expected = { ruleType: 'saved_query', anomalyThreshold: 50, + dataSourceType: 'indexPatterns', + dataViewId: undefined, machineLearningJobId: [], index: ['auditbeat-*'], queryBar: { @@ -266,6 +270,8 @@ describe('rule helpers', () => { const expected = { ruleType: 'saved_query', anomalyThreshold: 50, + dataSourceType: 'indexPatterns', + dataViewId: undefined, machineLearningJobId: [], index: ['auditbeat-*'], queryBar: { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index 4ae39c29909d9..86a54e099e7b2 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -33,6 +33,7 @@ import type { ScheduleStepRule, ActionsStepRule, } from './types'; +import { DataSourceType } from './types'; import { severityOptions } from '../../../components/rules/step_about_rule/data'; export interface GetStepsData { @@ -120,6 +121,7 @@ export const getDefineStepsData = (rule: Rule): DefineStepRule => ({ eventCategoryField: rule.event_category_override, tiebreakerField: rule.tiebreaker_field, }, + dataSourceType: rule.data_view_id ? DataSourceType.DataView : DataSourceType.IndexPatterns, newTermsFields: rule.new_terms_fields ?? [], historyWindowSize: rule.history_window_start ? convertHistoryStartToSize(rule.history_window_start) diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts index cd4a41d1d3f19..6e7dfe76bf5b1 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts @@ -133,6 +133,11 @@ export interface AboutStepRiskScore { isMappingChecked: boolean; } +export enum DataSourceType { + IndexPatterns = 'indexPatterns', + DataView = 'dataView', +} + /** * add / update data source types to show XOR relationship between 'index' and 'dataViewId' fields * Maybe something with io-ts? @@ -153,6 +158,7 @@ export interface DefineStepRule { threatQueryBar: FieldValueQueryBar; threatMapping: ThreatMapping; eqlOptions: EqlOptionsSelected; + dataSourceType: DataSourceType; newTermsFields: string[]; historyWindowSize: string; }