[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 <nikita.khristinin@elastic.co>
This commit is contained in:
Devin W. Hurley 2022-07-25 14:42:05 -04:00 committed by GitHub
parent 0824234fe1
commit c2985c4daa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 157 additions and 67 deletions

View file

@ -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<string, DataViewListItem>;
field: FieldHook<DefineStepRule['dataViewId']>;
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<DataViewListItem>();
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<EuiComboBoxOptionOption<string>>) => {
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);
}
};

View file

@ -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;

View file

@ -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',
};

View file

@ -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<StepDefineRuleProps> = ({
const [openTimelineSearch, setOpenTimelineSearch] = useState(false);
const [indexModified, setIndexModified] = useState(false);
const [threatIndexModified, setThreatIndexModified] = useState(false);
const [dataViewTitle, setDataViewTitle] = useState<string>();
const [indicesConfig] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY);
const [threatIndicesConfig] = useUiSetting$<string[]>(DEFAULT_THREAT_INDEX_KEY);
@ -202,6 +207,7 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
threatMapping: formThreatMapping,
machineLearningJobId: formMachineLearningJobId,
anomalyThreshold: formAnomalyThreshold,
dataSourceType: formDataSourceType,
newTermsFields: formNewTermsFields,
historyWindowSize: formHistoryWindowSize,
},
@ -221,6 +227,7 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
'threatMapping',
'machineLearningJobId',
'anomalyThreshold',
'dataSourceType',
'newTermsFields',
'historyWindowSize',
],
@ -236,6 +243,7 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
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<StepDefineRuleProps> = ({
const [optionsSelected, setOptionsSelected] = useState<EqlOptionsSelected>(
defaultValues?.eqlOptions || {}
);
const [initIsIndexPatternLoading, { browserFields, indexPatterns: initIndexPattern }] =
useFetchIndex(index, false);
const [indexPattern, setIndexPattern] = useState<DataViewBase>(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<DataViewBase>(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<DataViewFieldBase[]>([]);
@ -433,28 +468,26 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
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<StepDefineRuleProps> = ({
component={DataViewSelector}
componentProps={{
kibanaDataViews,
setIndexPattern,
setIsIndexPatternLoading,
}}
/>
);
@ -503,7 +534,7 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
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<StepDefineRuleProps> = ({
</EuiFlexItem>
<EuiFlexItem>
{dataSourceRadioIdSelected === DATA_VIEW_SELECT_ID ? (
DataViewSelectorMemo
) : (
<StyledVisibleContainer isVisible={dataSourceType === DataSourceType.DataView}>
{DataViewSelectorMemo}
</StyledVisibleContainer>
<StyledVisibleContainer isVisible={dataSourceType === DataSourceType.IndexPatterns}>
<CommonUseField
path="index"
config={{
@ -534,17 +566,18 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
},
}}
/>
)}
</StyledVisibleContainer>
</EuiFlexItem>
</EuiFlexGroup>
</RuleTypeEuiFormRow>
);
}, [
dataSourceRadioIdSelected,
dataSourceType,
dataViewIndexPatternToggleButtonOptions,
DataViewSelectorMemo,
indexModified,
handleResetIndices,
onChangeDataSource,
]);
const QueryBarMemo = useMemo(
@ -619,19 +652,36 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
[indexPattern]
);
const dataForDescription: Partial<DefineStepRule> = getStepDataDataSource(initialState);
if (dataSourceType === DataSourceType.DataView) {
dataForDescription.dataViewTitle = dataViewTitle;
}
return isReadOnlyView ? (
<StepContentWrapper data-test-subj="definitionRule" addPadding={addPadding}>
<StepRuleDescription
columns={descriptionColumns}
indexPatterns={indexPattern}
schema={filterRuleFieldsForType(schema, ruleType)}
data={filterRuleFieldsForType(initialState, ruleType)}
data={filterRuleFieldsForType(dataForDescription, ruleType)}
/>
</StepContentWrapper>
) : (
<>
<StepContentWrapper addPadding={!isUpdateView}>
<Form form={form} data-test-subj="stepDefineRule">
<StyledVisibleContainer isVisible={false}>
<UseField
path="dataSourceType"
componentProps={{
euiFieldProps: {
fullWidth: true,
placeholder: '',
},
}}
/>
</StyledVisibleContainer>
<UseField
path="ruleType"
component={SelectRuleType}

View file

@ -27,6 +27,7 @@ import type { FieldValueQueryBar } from '../query_bar';
import type { ERROR_CODE, FormSchema, ValidationFunc } from '../../../../shared_imports';
import { FIELD_TYPES, fieldValidators } from '../../../../shared_imports';
import type { DefineStepRule } from '../../../pages/detection_engine/rules/types';
import { DataSourceType } from '../../../pages/detection_engine/rules/types';
import { debounceAsync, eqlValidator } from '../eql_query_bar/validators';
import {
CUSTOM_QUERY_REQUIRED,
@ -56,7 +57,8 @@ export const schema: FormSchema<DefineStepRule> = {
...args: Parameters<ValidationFunc>
): ReturnType<ValidationFunc<{}, ERROR_CODE>> | 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<DefineStepRule> = {
// 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;

View file

@ -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',
});

View file

@ -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<DefineStepRule, 'dataViewId' | 'index' | 'dataSourceType'> & {
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 &&

View file

@ -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: {

View file

@ -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)

View file

@ -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;
}