diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx index 7734344d193b8..1ff5d770521f3 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx @@ -8,14 +8,27 @@ import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; +// we don't have the types for waitFor just yet, so using "as waitFor" until when we do +import { wait as waitFor } from '@testing-library/react'; import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks.ts'; -import { AutocompleteFieldListsComponent } from './field_value_lists'; +import { ListSchema } from '../../../lists_plugin_deps'; import { getFoundListSchemaMock } from '../../../../../lists/common/schemas/response/found_list_schema.mock'; +import { getListResponseMock } from '../../../../../lists/common/schemas/response/list_schema.mock'; +import { DATE_NOW } from '../../../../../lists/common/constants.mock'; + +import { AutocompleteFieldListsComponent } from './field_value_lists'; -const mockStart = jest.fn(); -const mockResult = getFoundListSchemaMock(); jest.mock('../../../common/lib/kibana'); +const mockStart = jest.fn(); +const mockKeywordList: ListSchema = { + ...getListResponseMock(), + id: 'keyword_list', + type: 'keyword', + name: 'keyword list', +}; +const mockResult = { ...getFoundListSchemaMock() }; +mockResult.data = [...mockResult.data, mockKeywordList]; jest.mock('../../../lists_plugin_deps', () => { const originalModule = jest.requireActual('../../../lists_plugin_deps'); @@ -31,7 +44,7 @@ jest.mock('../../../lists_plugin_deps', () => { }); describe('AutocompleteFieldListsComponent', () => { - test('it renders disabled if "isDisabled" is true', () => { + test('it renders disabled if "isDisabled" is true', async () => { const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { ); - expect( - wrapper - .find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] input`) - .prop('disabled') - ).toBeTruthy(); + await waitFor(() => { + expect( + wrapper + .find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] input`) + .prop('disabled') + ).toBeTruthy(); + }); }); - test('it renders loading if "isLoading" is true', () => { + test('it renders loading if "isLoading" is true', async () => { const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { /> ); - wrapper - .find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] button`) - .at(0) - .simulate('click'); - expect( + + await waitFor(() => { wrapper - .find( - `EuiComboBoxOptionsList[data-test-subj="valuesAutocompleteComboBox listsComboxBox-optionsList"]` - ) - .prop('isLoading') - ).toBeTruthy(); + .find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] button`) + .at(0) + .simulate('click'); + expect( + wrapper + .find( + `EuiComboBoxOptionsList[data-test-subj="valuesAutocompleteComboBox listsComboxBox-optionsList"]` + ) + .prop('isLoading') + ).toBeTruthy(); + }); }); - test('it allows user to clear values if "isClearable" is true', () => { + test('it allows user to clear values if "isClearable" is true', async () => { const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { /> ); - expect( wrapper .find(`[data-test-subj="comboBoxInput"]`) @@ -102,7 +119,55 @@ describe('AutocompleteFieldListsComponent', () => { ).toBeTruthy(); }); - test('it correctly displays selected list', () => { + test('it correctly displays lists that match the selected "keyword" field esType', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper.find('[data-test-subj="comboBoxToggleListButton"] button').simulate('click'); + + expect( + wrapper + .find('EuiComboBox[data-test-subj="valuesAutocompleteComboBox listsComboxBox"]') + .prop('options') + ).toEqual([{ label: 'keyword list' }]); + }); + + test('it correctly displays lists that match the selected "ip" field esType', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper.find('[data-test-subj="comboBoxToggleListButton"] button').simulate('click'); + + expect( + wrapper + .find('EuiComboBox[data-test-subj="valuesAutocompleteComboBox listsComboxBox"]') + .prop('options') + ).toEqual([{ label: 'some name' }]); + }); + + test('it correctly displays selected list', async () => { const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { }).onChange([{ label: 'some name' }]); expect(mockOnChange).toHaveBeenCalledWith({ - created_at: '2020-04-20T15:25:31.830Z', + created_at: DATE_NOW, created_by: 'some user', description: 'some description', id: 'some-list-id', @@ -154,7 +219,7 @@ describe('AutocompleteFieldListsComponent', () => { name: 'some name', tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e', type: 'ip', - updated_at: '2020-04-20T15:25:31.830Z', + updated_at: DATE_NOW, updated_by: 'some user', }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx index d8ce27e97874d..a9d85452651b5 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx @@ -36,8 +36,12 @@ export const AutocompleteFieldListsComponent: React.FC name, []); const optionsMemo = useMemo(() => { - if (selectedField != null) { - return lists.filter(({ type }) => type === selectedField.type); + if ( + selectedField != null && + selectedField.esTypes != null && + selectedField.esTypes.length > 0 + ) { + return lists.filter(({ type }) => selectedField.esTypes?.includes(type)); } else { return []; } diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx index 32a82af114bae..a082811920f88 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx @@ -79,10 +79,10 @@ export const AutocompleteFieldMatchComponent: React.FC validateParams(selectedValue, selectedField ? selectedField.type : ''), - [selectedField, selectedValue] - ); + const isValid = useMemo((): boolean => validateParams(selectedValue, selectedField), [ + selectedField, + selectedValue, + ]); return ( { const areAnyInvalid = selectedComboOptions.filter( - ({ label }) => !validateParams(label, selectedField ? selectedField.type : '') + ({ label }) => !validateParams(label, selectedField) ); return areAnyInvalid.length === 0; }, [selectedComboOptions, selectedField]); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts index cfe23b9391ec0..cb07d99913107 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts @@ -55,49 +55,25 @@ describe('helpers', () => { describe('#validateParams', () => { test('returns true if value is undefined', () => { - const isValid = validateParams(undefined, 'date'); + const isValid = validateParams(undefined, getField('@timestamp')); expect(isValid).toBeTruthy(); }); test('returns true if value is empty string', () => { - const isValid = validateParams('', 'date'); + const isValid = validateParams('', getField('@timestamp')); expect(isValid).toBeTruthy(); }); test('returns true if type is "date" and value is valid', () => { - const isValid = validateParams('1994-11-05T08:15:30-05:00', 'date'); + const isValid = validateParams('1994-11-05T08:15:30-05:00', getField('@timestamp')); expect(isValid).toBeTruthy(); }); test('returns false if type is "date" and value is not valid', () => { - const isValid = validateParams('1593478826', 'date'); - - expect(isValid).toBeFalsy(); - }); - - test('returns true if type is "ip" and value is valid', () => { - const isValid = validateParams('126.45.211.34', 'ip'); - - expect(isValid).toBeTruthy(); - }); - - test('returns false if type is "ip" and value is not valid', () => { - const isValid = validateParams('hellooo', 'ip'); - - expect(isValid).toBeFalsy(); - }); - - test('returns true if type is "number" and value is valid', () => { - const isValid = validateParams('123', 'number'); - - expect(isValid).toBeTruthy(); - }); - - test('returns false if type is "number" and value is not valid', () => { - const isValid = validateParams('not a number', 'number'); + const isValid = validateParams('1593478826', getField('@timestamp')); expect(isValid).toBeFalsy(); }); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts index 483ca5d6d332e..16659593784db 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts @@ -7,7 +7,7 @@ import dateMath from '@elastic/datemath'; import { EuiComboBoxOptionOption } from '@elastic/eui'; -import { IFieldType, Ipv4Address } from '../../../../../../../src/plugins/data/common'; +import { IFieldType } from '../../../../../../../src/plugins/data/common'; import { EXCEPTION_OPERATORS, @@ -30,29 +30,27 @@ export const getOperators = (field: IFieldType | undefined): OperatorOption[] => } }; -export function validateParams(params: string | undefined, type: string) { +export const validateParams = ( + params: string | undefined, + field: IFieldType | undefined +): boolean => { // Box would show error state if empty otherwise if (params == null || params === '') { return true; } - switch (type) { - case 'date': - const moment = dateMath.parse(params); - return Boolean(moment && moment.isValid()); - case 'ip': - try { - return Boolean(new Ipv4Address(params)); - } catch (e) { - return false; - } - case 'number': - const val = parseFloat(params); - return typeof val === 'number' && !isNaN(val); - default: - return true; - } -} + const types = field != null && field.esTypes != null ? field.esTypes : []; + + return types.reduce((acc, type) => { + switch (type) { + case 'date': + const moment = dateMath.parse(params); + return Boolean(moment && moment.isValid()); + default: + return acc; + } + }, true); +}; export function getGenericComboBoxProps({ options, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index 53c53f48f076b..e630645ef8c4e 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -22,7 +22,6 @@ import { EuiText, } from '@elastic/eui'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; -import { alertsIndexPattern } from '../../../../../common/endpoint/constants'; import { ExceptionListItemSchema, CreateExceptionListItemSchema, @@ -48,24 +47,18 @@ import { } from '../helpers'; import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules'; -export interface AddExceptionOnClick { +export interface AddExceptionModalBaseProps { ruleName: string; ruleId: string; exceptionListType: ExceptionListType; + ruleIndices: string[]; alertData?: { ecsData: Ecs; nonEcsData: TimelineNonEcsData[]; }; } -interface AddExceptionModalProps { - ruleName: string; - ruleId: string; - exceptionListType: ExceptionListType; - alertData?: { - ecsData: Ecs; - nonEcsData: TimelineNonEcsData[]; - }; +export interface AddExceptionModalProps extends AddExceptionModalBaseProps { onCancel: () => void; onConfirm: (didCloseAlert: boolean) => void; alertStatus?: Status; @@ -78,10 +71,8 @@ const Modal = styled(EuiModal)` `; const ModalHeader = styled(EuiModalHeader)` - ${({ theme }) => css` - flex-direction: column; - align-items: flex-start; - `} + flex-direction: column; + align-items: flex-start; `; const ModalHeaderSubtitle = styled.div` @@ -103,6 +94,7 @@ const ModalBodySection = styled.section` export const AddExceptionModal = memo(function AddExceptionModal({ ruleName, ruleId, + ruleIndices, exceptionListType, alertData, onCancel, @@ -120,10 +112,11 @@ export const AddExceptionModal = memo(function AddExceptionModal({ const [fetchOrCreateListError, setFetchOrCreateListError] = useState(false); const { addError, addSuccess } = useAppToasts(); const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex(); + const [ + { isLoading: isSignalIndexPatternLoading, indexPatterns: signalIndexPatterns }, + ] = useFetchIndexPatterns(signalIndexName !== null ? [signalIndexName] : []); - const [{ isLoading: indexPatternLoading, indexPatterns }] = useFetchIndexPatterns( - signalIndexName !== null ? [signalIndexName] : [] - ); + const [{ isLoading: isIndexPatternLoading, indexPatterns }] = useFetchIndexPatterns(ruleIndices); const onError = useCallback( (error: Error) => { @@ -183,19 +176,19 @@ export const AddExceptionModal = memo(function AddExceptionModal({ }, [alertData, exceptionListType, ruleExceptionList, ruleName]); useEffect(() => { - if (indexPatternLoading === false && isSignalIndexLoading === false) { + if (isSignalIndexPatternLoading === false && isSignalIndexLoading === false) { setShouldDisableBulkClose( entryHasListType(exceptionItemsToAdd) || - entryHasNonEcsType(exceptionItemsToAdd, indexPatterns) || + entryHasNonEcsType(exceptionItemsToAdd, signalIndexPatterns) || exceptionItemsToAdd.length === 0 ); } }, [ setShouldDisableBulkClose, exceptionItemsToAdd, - indexPatternLoading, + isSignalIndexPatternLoading, isSignalIndexLoading, - indexPatterns, + signalIndexPatterns, ]); useEffect(() => { @@ -274,15 +267,8 @@ export const AddExceptionModal = memo(function AddExceptionModal({ [fetchOrCreateListError, exceptionItemsToAdd] ); - const indexPatternConfig = useCallback(() => { - if (exceptionListType === 'endpoint') { - return [alertsIndexPattern]; - } - return signalIndexName ? [signalIndexName] : []; - }, [exceptionListType, signalIndexName]); - return ( - + {i18n.ADD_EXCEPTION} @@ -301,8 +287,9 @@ export const AddExceptionModal = memo(function AddExceptionModal({ )} {fetchOrCreateListError === false && !isSignalIndexLoading && - !indexPatternLoading && + !isSignalIndexPatternLoading && !isLoadingExceptionList && + !isIndexPatternLoading && ruleExceptionList && ( <> @@ -314,8 +301,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ listId={ruleExceptionList.list_id} listNamespaceType={ruleExceptionList.namespace_type} ruleName={ruleName} - indexPatternConfig={indexPatternConfig()} - isLoading={false} + indexPatterns={indexPatterns} isOrDisabled={false} isAndDisabled={false} data-test-subj="alert-exception-builder" diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx index 08e5b49073ecf..f6feca591dc6d 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx @@ -3,12 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState, useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import { ExceptionListItemComponent } from './builder_exception_item'; -import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules/fetch_index_patterns'; +import { IIndexPattern } from '../../../../../../../../src/plugins/data/common'; import { ExceptionListItemSchema, NamespaceType, @@ -22,7 +22,6 @@ import { AndOrBadge } from '../../and_or_badge'; import { BuilderButtonOptions } from './builder_button_options'; import { getNewExceptionItem, filterExceptionItems } from '../helpers'; import { ExceptionsBuilderExceptionItem, CreateExceptionListItemBuilderSchema } from '../types'; -import { Loader } from '../../loader'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import exceptionableFields from '../exceptionable_fields.json'; @@ -51,8 +50,7 @@ interface ExceptionBuilderProps { listId: string; listNamespaceType: NamespaceType; ruleName: string; - indexPatternConfig: string[]; - isLoading: boolean; + indexPatterns: IIndexPattern; isOrDisabled: boolean; isAndDisabled: boolean; onChange: (arg: OnChangeProps) => void; @@ -64,8 +62,7 @@ export const ExceptionBuilder = ({ listId, listNamespaceType, ruleName, - indexPatternConfig, - isLoading, + indexPatterns, isOrDisabled, isAndDisabled, onChange, @@ -75,9 +72,6 @@ export const ExceptionBuilder = ({ exceptionListItems ); const [exceptionsToDelete, setExceptionsToDelete] = useState([]); - const [{ isLoading: indexPatternLoading, indexPatterns }] = useFetchIndexPatterns( - indexPatternConfig ?? [] - ); const handleCheckAndLogic = (items: ExceptionsBuilderExceptionItem[]): void => { setAndLogicIncluded(items.filter(({ entries }) => entries.length > 1).length > 0); @@ -154,7 +148,7 @@ export const ExceptionBuilder = ({ }, [setExceptions, listType, listId, listNamespaceType, ruleName]); // Filters index pattern fields by exceptionable fields if list type is endpoint - const filterIndexPatterns = useCallback(() => { + const filterIndexPatterns = useMemo((): IIndexPattern => { if (listType === 'endpoint') { return { ...indexPatterns, @@ -196,9 +190,6 @@ export const ExceptionBuilder = ({ return ( - {(isLoading || indexPatternLoading) && ( - - )} {exceptions.map((exceptionListItem, index) => ( @@ -224,8 +215,8 @@ export const ExceptionBuilder = ({ key={getExceptionListItemId(exceptionListItem, index)} exceptionItem={exceptionListItem} exceptionId={getExceptionListItemId(exceptionListItem, index)} - indexPattern={filterIndexPatterns()} - isLoading={indexPatternLoading} + indexPattern={filterIndexPatterns} + isLoading={indexPatterns.fields.length === 0} exceptionItemIndex={index} andLogicIncluded={andLogicIncluded} isOnlyItem={exceptions.length === 1} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index 51cc684a01de6..d07a8b5f0d2f6 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -20,7 +20,6 @@ import { EuiFormRow, EuiText, } from '@elastic/eui'; -import { alertsIndexPattern } from '../../../../../common/endpoint/constants'; import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; import { @@ -45,6 +44,7 @@ import { Loader } from '../../loader'; interface EditExceptionModalProps { ruleName: string; + ruleIndices: string[]; exceptionItem: ExceptionListItemSchema; exceptionListType: ExceptionListType; onCancel: () => void; @@ -58,10 +58,8 @@ const Modal = styled(EuiModal)` `; const ModalHeader = styled(EuiModalHeader)` - ${({ theme }) => css` - flex-direction: column; - align-items: flex-start; - `} + flex-direction: column; + align-items: flex-start; `; const ModalHeaderSubtitle = styled.div` @@ -82,6 +80,7 @@ const ModalBodySection = styled.section` export const EditExceptionModal = memo(function EditExceptionModal({ ruleName, + ruleIndices, exceptionItem, exceptionListType, onCancel, @@ -96,10 +95,11 @@ export const EditExceptionModal = memo(function EditExceptionModal({ >([]); const { addError, addSuccess } = useAppToasts(); const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex(); + const [ + { isLoading: isSignalIndexPatternLoading, indexPatterns: signalIndexPatterns }, + ] = useFetchIndexPatterns(signalIndexName !== null ? [signalIndexName] : []); - const [{ isLoading: indexPatternLoading, indexPatterns }] = useFetchIndexPatterns( - signalIndexName !== null ? [signalIndexName] : [] - ); + const [{ isLoading: isIndexPatternLoading, indexPatterns }] = useFetchIndexPatterns(ruleIndices); const onError = useCallback( (error) => { @@ -122,19 +122,19 @@ export const EditExceptionModal = memo(function EditExceptionModal({ ); useEffect(() => { - if (indexPatternLoading === false && isSignalIndexLoading === false) { + if (isSignalIndexPatternLoading === false && isSignalIndexLoading === false) { setShouldDisableBulkClose( entryHasListType(exceptionItemsToAdd) || - entryHasNonEcsType(exceptionItemsToAdd, indexPatterns) || + entryHasNonEcsType(exceptionItemsToAdd, signalIndexPatterns) || exceptionItemsToAdd.length === 0 ); } }, [ setShouldDisableBulkClose, exceptionItemsToAdd, - indexPatternLoading, + isSignalIndexPatternLoading, isSignalIndexLoading, - indexPatterns, + signalIndexPatterns, ]); useEffect(() => { @@ -189,15 +189,8 @@ export const EditExceptionModal = memo(function EditExceptionModal({ } }, [addOrUpdateExceptionItems, enrichExceptionItems, shouldBulkCloseAlert, signalIndexName]); - const indexPatternConfig = useCallback(() => { - if (exceptionListType === 'endpoint') { - return [alertsIndexPattern]; - } - return signalIndexName ? [signalIndexName] : []; - }, [exceptionListType, signalIndexName]); - return ( - + {i18n.EDIT_EXCEPTION_TITLE} @@ -206,11 +199,11 @@ export const EditExceptionModal = memo(function EditExceptionModal({ - {(addExceptionIsLoading || indexPatternLoading || isSignalIndexLoading) && ( + {(addExceptionIsLoading || isIndexPatternLoading || isSignalIndexLoading) && ( )} - {!isSignalIndexLoading && !addExceptionIsLoading && !indexPatternLoading && ( + {!isSignalIndexLoading && !addExceptionIsLoading && !isIndexPatternLoading && ( <> {i18n.EXCEPTION_BUILDER_INFO} @@ -221,13 +214,12 @@ export const EditExceptionModal = memo(function EditExceptionModal({ listId={exceptionItem.list_id} listNamespaceType={exceptionItem.namespace_type} ruleName={ruleName} - isLoading={false} isOrDisabled={false} isAndDisabled={false} data-test-subj="edit-exception-modal-builder" id-aria="edit-exception-modal-builder" onChange={handleBuilderOnChange} - indexPatternConfig={indexPatternConfig()} + indexPatterns={indexPatterns} /> diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx index f72008cbdffe1..986f27f6495ec 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx @@ -67,6 +67,7 @@ describe('ExceptionsViewer', () => { ({ eui: euiLightVars, darkMode: false })}> { const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { + }: UseExceptionListSuccess): void => { dispatch({ type: 'setExceptions', lists: newLists, @@ -253,10 +255,11 @@ const ExceptionsViewerComponent = ({ return ( <> {currentModal === 'editModal' && - exceptionToEdit !== null && - exceptionListTypeToEdit !== null && ( + exceptionToEdit != null && + exceptionListTypeToEdit != null && ( 0 ? { fields: fields.map((field) => - pick(['name', 'searchable', 'type', 'aggregatable'], field) + pick(['name', 'searchable', 'type', 'aggregatable', 'esTypes', 'subType'], field) ), title, } 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 71cf5c10de764..a4ce6c0200eb3 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 @@ -12,6 +12,7 @@ import { Dispatch } from 'redux'; import { EuiText } from '@elastic/eui'; import { RowRendererId } from '../../../../common/types/timeline'; +import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { Filter } from '../../../../../../../src/plugins/data/common/es_query'; import { @@ -38,7 +39,7 @@ import { UpdateTimelineLoading, } from './types'; import { Ecs, TimelineNonEcsData } from '../../../graphql/types'; -import { AddExceptionOnClick } from '../../../common/components/exceptions/add_exception_modal'; +import { AddExceptionModalBaseProps } from '../../../common/components/exceptions/add_exception_modal'; import { getMappedNonEcsValue } from '../../../common/components/exceptions/helpers'; export const buildAlertStatusFilter = (status: Status): Filter[] => [ @@ -225,7 +226,7 @@ interface AlertActionArgs { alertData, ruleName, ruleId, - }: AddExceptionOnClick) => void; + }: AddExceptionModalBaseProps) => void; } export const getAlertActions = ({ @@ -346,10 +347,12 @@ export const getAlertActions = ({ onClick: ({ ecsData, data }: TimelineRowActionOnClick) => { const [ruleName] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.name' }); const [ruleId] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.id' }); + const ruleIndices = getMappedNonEcsValue({ data, fieldName: 'signal.rule.index' }); if (ruleId !== undefined) { openAddExceptionModal({ ruleName: ruleName ?? '', ruleId, + ruleIndices: ruleIndices.length > 0 ? ruleIndices : DEFAULT_INDEX_PATTERN, exceptionListType: 'endpoint', alertData: { ecsData, @@ -369,10 +372,12 @@ export const getAlertActions = ({ onClick: ({ ecsData, data }: TimelineRowActionOnClick) => { const [ruleName] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.name' }); const [ruleId] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.id' }); + const ruleIndices = getMappedNonEcsValue({ data, fieldName: 'signal.rule.index' }); if (ruleId !== undefined) { openAddExceptionModal({ ruleName: ruleName ?? '', ruleId, + ruleIndices: ruleIndices.length > 0 ? ruleIndices : DEFAULT_INDEX_PATTERN, exceptionListType: 'detection', alertData: { ecsData, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 1d4c97d85443f..1eda358fe5944 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -54,7 +54,7 @@ import { import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers'; import { AddExceptionModal, - AddExceptionOnClick, + AddExceptionModalBaseProps, } from '../../../common/components/exceptions/add_exception_modal'; interface OwnProps { @@ -73,9 +73,10 @@ interface OwnProps { type AlertsTableComponentProps = OwnProps & PropsFromRedux; -const addExceptionModalInitialState: AddExceptionOnClick = { +const addExceptionModalInitialState: AddExceptionModalBaseProps = { ruleName: '', ruleId: '', + ruleIndices: [], exceptionListType: 'detection', alertData: undefined, }; @@ -112,7 +113,7 @@ export const AlertsTableComponent: React.FC = ({ const [showClearSelectionAction, setShowClearSelectionAction] = useState(false); const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); const [shouldShowAddExceptionModal, setShouldShowAddExceptionModal] = useState(false); - const [addExceptionModalState, setAddExceptionModalState] = useState( + const [addExceptionModalState, setAddExceptionModalState] = useState( addExceptionModalInitialState ); const [{ browserFields, indexPatterns }] = useFetchIndexPatterns( @@ -216,12 +217,19 @@ export const AlertsTableComponent: React.FC = ({ ); const openAddExceptionModalCallback = useCallback( - ({ ruleName, ruleId, exceptionListType, alertData }: AddExceptionOnClick) => { + ({ + ruleName, + ruleIndices, + ruleId, + exceptionListType, + alertData, + }: AddExceptionModalBaseProps) => { if (alertData !== null && alertData !== undefined) { setShouldShowAddExceptionModal(true); setAddExceptionModalState({ ruleName, ruleId, + ruleIndices, exceptionListType, alertData, }); @@ -421,12 +429,9 @@ export const AlertsTableComponent: React.FC = ({ closeAddExceptionModal(); }, [closeAddExceptionModal]); - const onAddExceptionConfirm = useCallback( - (didCloseAlert: boolean) => { - closeAddExceptionModal(); - }, - [closeAddExceptionModal] - ); + const onAddExceptionConfirm = useCallback(() => closeAddExceptionModal(), [ + closeAddExceptionModal, + ]); if (loading || isEmpty(signalsIndex)) { return ( @@ -454,6 +459,7 @@ export const AlertsTableComponent: React.FC = ({ = ({ ; format?: Maybe; + /** the elastic type as mapped in the index */ + esTypes?: Maybe; + + subType?: Maybe; } export interface AuthenticationsData { @@ -2780,6 +2788,10 @@ export namespace SourceQuery { aggregatable: boolean; format: Maybe; + + esTypes: Maybe; + + subType: Maybe; }; } diff --git a/x-pack/plugins/security_solution/server/graphql/ecs/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/ecs/resolvers.ts index f30b7d192d05d..414e5b5d95bec 100644 --- a/x-pack/plugins/security_solution/server/graphql/ecs/resolvers.ts +++ b/x-pack/plugins/security_solution/server/graphql/ecs/resolvers.ts @@ -47,9 +47,41 @@ export const toStringArrayScalar = new GraphQLScalarType({ return null; }, }); - +export const toStringArrayNoNullableScalar = new GraphQLScalarType({ + name: 'StringArray', + description: 'Represents value in detail item from the timeline who wants to more than one type', + serialize(value): string[] | undefined { + if (value == null) { + return undefined; + } else if (Array.isArray(value)) { + return convertArrayToString(value) as string[]; + } else if (isBoolean(value) || isNumber(value) || isObject(value)) { + return [convertToString(value)]; + } + return [value]; + }, + parseValue(value) { + return value; + }, + parseLiteral(ast) { + switch (ast.kind) { + case Kind.INT: + return parseInt(ast.value, 10); + case Kind.FLOAT: + return parseFloat(ast.value); + case Kind.STRING: + return ast.value; + case Kind.LIST: + return ast.values; + case Kind.OBJECT: + return ast.fields; + } + return undefined; + }, +}); export const createScalarToStringArrayValueResolvers = () => ({ ToStringArray: toStringArrayScalar, + ToStringArrayNoNullable: toStringArrayNoNullableScalar, }); const convertToString = (value: object | number | boolean | string): string => { diff --git a/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts index 5b093a02b6514..bdc69f85d3542 100644 --- a/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts @@ -8,6 +8,7 @@ import gql from 'graphql-tag'; export const ecsSchema = gql` scalar ToStringArray + scalar ToStringArrayNoNullable type EventEcsFields { action: ToStringArray diff --git a/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.ts index 24589822f0250..8d55e645d6791 100644 --- a/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.ts +++ b/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.ts @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { GraphQLScalarType, Kind } from 'graphql'; import { SourceStatusResolvers } from '../../graphql/types'; import { AppResolverOf, ChildResolverOf } from '../../lib/framework'; import { IndexFields } from '../../lib/index_fields'; import { SourceStatus } from '../../lib/source_status'; import { QuerySourceResolver } from '../sources/resolvers'; +import { IFieldSubType } from '../../../../../../src/plugins/data/common/index_patterns/types'; export type SourceStatusIndicesExistResolver = ChildResolverOf< AppResolverOf, @@ -50,3 +52,40 @@ export const createSourceStatusResolvers = (libs: { }, }, }); + +export const toIFieldSubTypeNonNullableScalar = new GraphQLScalarType({ + name: 'IFieldSubType', + description: 'Represents value in index pattern field item', + serialize(value): IFieldSubType | undefined { + if (value == null) { + return undefined; + } + + return { + multi: value.multi ?? undefined, + nested: value.nested ?? undefined, + }; + }, + parseValue(value) { + return value; + }, + parseLiteral(ast) { + switch (ast.kind) { + case Kind.INT: + return undefined; + case Kind.FLOAT: + return undefined; + case Kind.STRING: + return undefined; + case Kind.LIST: + return undefined; + case Kind.OBJECT: + return ast; + } + return undefined; + }, +}); + +export const createScalarToIFieldSubTypeNonNullableScalarResolvers = () => ({ + ToIFieldSubTypeNonNullable: toIFieldSubTypeNonNullableScalar, +}); diff --git a/x-pack/plugins/security_solution/server/graphql/source_status/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/source_status/schema.gql.ts index e484b60f8f364..3062113f1b635 100644 --- a/x-pack/plugins/security_solution/server/graphql/source_status/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/source_status/schema.gql.ts @@ -7,6 +7,8 @@ import gql from 'graphql-tag'; export const sourceStatusSchema = gql` + scalar ToIFieldSubTypeNonNullable + "A descriptor of a field in an index" type IndexField { "Where the field belong" @@ -26,6 +28,9 @@ export const sourceStatusSchema = gql` "Description of the field" description: String format: String + "the elastic type as mapped in the index" + esTypes: ToStringArrayNoNullable + subType: ToIFieldSubTypeNonNullable } extend type SourceStatus { diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index f8a614e86f28e..1e397a4e6bb6c 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -430,6 +430,10 @@ export enum FlowDirection { biDirectional = 'biDirectional', } +export type ToStringArrayNoNullable = any; + +export type ToIFieldSubTypeNonNullable = any; + export type ToStringArray = string[] | string; export type Date = string; @@ -629,6 +633,10 @@ export interface IndexField { description?: Maybe; format?: Maybe; + /** the elastic type as mapped in the index */ + esTypes?: Maybe; + + subType?: Maybe; } export interface AuthenticationsData { @@ -3579,6 +3587,10 @@ export namespace IndexFieldResolvers { description?: DescriptionResolver, TypeParent, TContext>; format?: FormatResolver, TypeParent, TContext>; + /** the elastic type as mapped in the index */ + esTypes?: EsTypesResolver, TypeParent, TContext>; + + subType?: SubTypeResolver, TypeParent, TContext>; } export type CategoryResolver = Resolver< @@ -3626,6 +3638,16 @@ export namespace IndexFieldResolvers { Parent = IndexField, TContext = SiemContext > = Resolver; + export type EsTypesResolver< + R = Maybe, + Parent = IndexField, + TContext = SiemContext + > = Resolver; + export type SubTypeResolver< + R = Maybe, + Parent = IndexField, + TContext = SiemContext + > = Resolver; } export namespace AuthenticationsDataResolvers { @@ -9317,6 +9339,14 @@ export interface DeprecatedDirectiveArgs { reason?: string; } +export interface ToStringArrayNoNullableScalarConfig + extends GraphQLScalarTypeConfig { + name: 'ToStringArrayNoNullable'; +} +export interface ToIFieldSubTypeNonNullableScalarConfig + extends GraphQLScalarTypeConfig { + name: 'ToIFieldSubTypeNonNullable'; +} export interface ToStringArrayScalarConfig extends GraphQLScalarTypeConfig { name: 'ToStringArray'; } @@ -9490,6 +9520,8 @@ export type IResolvers = { EventsTimelineData?: EventsTimelineDataResolvers.Resolvers; OsFields?: OsFieldsResolvers.Resolvers; HostFields?: HostFieldsResolvers.Resolvers; + ToStringArrayNoNullable?: GraphQLScalarType; + ToIFieldSubTypeNonNullable?: GraphQLScalarType; ToStringArray?: GraphQLScalarType; Date?: GraphQLScalarType; ToNumberArray?: GraphQLScalarType; diff --git a/x-pack/plugins/security_solution/server/lib/index_fields/types.ts b/x-pack/plugins/security_solution/server/lib/index_fields/types.ts index 0c894c6980a31..67b3c254007e2 100644 --- a/x-pack/plugins/security_solution/server/lib/index_fields/types.ts +++ b/x-pack/plugins/security_solution/server/lib/index_fields/types.ts @@ -6,6 +6,7 @@ import { IndexField } from '../../graphql/types'; import { FrameworkRequest } from '../framework'; +import { IFieldSubType } from '../../../../../../src/plugins/data/common'; export interface FieldsAdapter { getIndexFields(req: FrameworkRequest, indices: string[]): Promise; @@ -16,4 +17,6 @@ export interface IndexFieldDescriptor { type: string; searchable: boolean; aggregatable: boolean; + esTypes?: string[]; + subType?: IFieldSubType; }