[Security Solution][Exceptions] - Make esTypes and subType available to index patterns (#72336)

## Summary

This PR updates the following:

- `useFetchIndexPatterns` now returns `indexPatterns` whose fields include `esTypes` and `subType`
  - Why?? The exceptions builder needs these two fields to determine what fields are of ES type `nested` and parent paths
- exceptions add and edit modals now use the `rule.index` field to pass into `useFetchindexPatterns`
  - Before we were using the signals index and alerts index for endpoint, needs to be rule's index patterns
  - if no index patterns exist on the rule (if rule created via API, it's not required), then uses `DEFAULT_INDEX_PATTERN`
- updates the autocomplete validation to use `IField.esTypes` to check type instead of `IField.type`
This commit is contained in:
Yara Tercero 2020-07-20 19:18:42 -04:00 committed by GitHub
parent 03fe8c3e89
commit 21977a7e6a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 362 additions and 169 deletions

View file

@ -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(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<AutocompleteFieldListsComponent
@ -46,14 +59,16 @@ describe('AutocompleteFieldListsComponent', () => {
</ThemeProvider>
);
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(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<AutocompleteFieldListsComponent
@ -67,20 +82,23 @@ describe('AutocompleteFieldListsComponent', () => {
/>
</ThemeProvider>
);
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(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<AutocompleteFieldListsComponent
@ -94,7 +112,6 @@ describe('AutocompleteFieldListsComponent', () => {
/>
</ThemeProvider>
);
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(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<AutocompleteFieldListsComponent
placeholder="Placeholder text"
selectedField={getField('@tags')}
selectedValue=""
isLoading={false}
isClearable={false}
isDisabled={false}
onChange={jest.fn()}
/>
</ThemeProvider>
);
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(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<AutocompleteFieldListsComponent
placeholder="Placeholder text"
selectedField={getField('ip')}
selectedValue=""
isLoading={false}
isClearable={false}
isDisabled={false}
onChange={jest.fn()}
/>
</ThemeProvider>
);
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(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<AutocompleteFieldListsComponent
@ -146,7 +211,7 @@ describe('AutocompleteFieldListsComponent', () => {
}).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',
});
});

View file

@ -36,8 +36,12 @@ export const AutocompleteFieldListsComponent: React.FC<AutocompleteFieldListsPro
const getLabel = useCallback(({ name }) => 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 [];
}

View file

@ -79,10 +79,10 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
});
};
const isValid = useMemo(
(): boolean => validateParams(selectedValue, selectedField ? selectedField.type : ''),
[selectedField, selectedValue]
);
const isValid = useMemo((): boolean => validateParams(selectedValue, selectedField), [
selectedField,
selectedValue,
]);
return (
<EuiComboBox

View file

@ -76,7 +76,7 @@ export const AutocompleteFieldMatchAnyComponent: React.FC<AutocompleteFieldMatch
const isValid = useMemo((): boolean => {
const areAnyInvalid = selectedComboOptions.filter(
({ label }) => !validateParams(label, selectedField ? selectedField.type : '')
({ label }) => !validateParams(label, selectedField)
);
return areAnyInvalid.length === 0;
}, [selectedComboOptions, selectedField]);

View file

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

View file

@ -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<boolean>((acc, type) => {
switch (type) {
case 'date':
const moment = dateMath.parse(params);
return Boolean(moment && moment.isValid());
default:
return acc;
}
}, true);
};
export function getGenericComboBoxProps<T>({
options,

View file

@ -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 (
<EuiOverlayMask>
<EuiOverlayMask onClick={onCancel}>
<Modal onClose={onCancel} data-test-subj="add-exception-modal">
<ModalHeader>
<EuiModalHeaderTitle>{i18n.ADD_EXCEPTION}</EuiModalHeaderTitle>
@ -301,8 +287,9 @@ export const AddExceptionModal = memo(function AddExceptionModal({
)}
{fetchOrCreateListError === false &&
!isSignalIndexLoading &&
!indexPatternLoading &&
!isSignalIndexPatternLoading &&
!isLoadingExceptionList &&
!isIndexPatternLoading &&
ruleExceptionList && (
<>
<ModalBodySection className="builder-section">
@ -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"

View file

@ -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<ExceptionListItemSchema[]>([]);
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 (
<EuiFlexGroup gutterSize="s" direction="column">
{(isLoading || indexPatternLoading) && (
<Loader data-test-subj="loadingPanelAllRulesTable" overlay size="xl" />
)}
{exceptions.map((exceptionListItem, index) => (
<EuiFlexItem grow={1} key={getExceptionListItemId(exceptionListItem, index)}>
<EuiFlexGroup gutterSize="s" direction="column">
@ -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}

View file

@ -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 (
<EuiOverlayMask>
<EuiOverlayMask onClick={onCancel}>
<Modal onClose={onCancel} data-test-subj="add-exception-modal">
<ModalHeader>
<EuiModalHeaderTitle>{i18n.EDIT_EXCEPTION_TITLE}</EuiModalHeaderTitle>
@ -206,11 +199,11 @@ export const EditExceptionModal = memo(function EditExceptionModal({
</ModalHeaderSubtitle>
</ModalHeader>
{(addExceptionIsLoading || indexPatternLoading || isSignalIndexLoading) && (
{(addExceptionIsLoading || isIndexPatternLoading || isSignalIndexLoading) && (
<Loader data-test-subj="loadingEditExceptionModal" size="xl" />
)}
{!isSignalIndexLoading && !addExceptionIsLoading && !indexPatternLoading && (
{!isSignalIndexLoading && !addExceptionIsLoading && !isIndexPatternLoading && (
<>
<ModalBodySection className="builder-section">
<EuiText>{i18n.EXCEPTION_BUILDER_INFO}</EuiText>
@ -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}
/>
<EuiSpacer />

View file

@ -67,6 +67,7 @@ describe('ExceptionsViewer', () => {
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionsViewer
ruleId={'123'}
ruleIndices={['filebeat-*']}
ruleName={ruleName}
exceptionListsMeta={[
{
@ -88,6 +89,7 @@ describe('ExceptionsViewer', () => {
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionsViewer
ruleIndices={['filebeat-*']}
ruleId={'123'}
ruleName={ruleName}
exceptionListsMeta={[]}
@ -116,6 +118,7 @@ describe('ExceptionsViewer', () => {
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionsViewer
ruleIndices={['filebeat-*']}
ruleId={'123'}
ruleName={ruleName}
exceptionListsMeta={[

View file

@ -53,6 +53,7 @@ const initialState: State = {
interface ExceptionsViewerProps {
ruleId: string;
ruleName: string;
ruleIndices: string[];
exceptionListsMeta: ExceptionIdentifiers[];
availableListTypes: ExceptionListTypeEnum[];
commentsAccordionId: string;
@ -61,6 +62,7 @@ interface ExceptionsViewerProps {
const ExceptionsViewerComponent = ({
ruleId,
ruleName,
ruleIndices,
exceptionListsMeta,
availableListTypes,
commentsAccordionId,
@ -104,7 +106,7 @@ const ExceptionsViewerComponent = ({
lists: newLists,
exceptions: newExceptions,
pagination: newPagination,
}: UseExceptionListSuccess) => {
}: UseExceptionListSuccess): void => {
dispatch({
type: 'setExceptions',
lists: newLists,
@ -253,10 +255,11 @@ const ExceptionsViewerComponent = ({
return (
<>
{currentModal === 'editModal' &&
exceptionToEdit !== null &&
exceptionListTypeToEdit !== null && (
exceptionToEdit != null &&
exceptionListTypeToEdit != null && (
<EditExceptionModal
ruleName={ruleName}
ruleIndices={ruleIndices}
exceptionListType={exceptionListTypeToEdit}
exceptionItem={exceptionToEdit}
onCancel={handleOnCancelExceptionModal}
@ -267,6 +270,7 @@ const ExceptionsViewerComponent = ({
{currentModal === 'addModal' && exceptionListTypeToEdit != null && (
<AddExceptionModal
ruleName={ruleName}
ruleIndices={ruleIndices}
ruleId={ruleId}
exceptionListType={exceptionListTypeToEdit}
onCancel={handleOnCancelExceptionModal}

View file

@ -22,6 +22,8 @@ export const sourceQuery = gql`
type
aggregatable
format
esTypes
subType
}
}
}

View file

@ -60,7 +60,7 @@ export const getIndexFields = memoizeOne(
fields && fields.length > 0
? {
fields: fields.map((field) =>
pick(['name', 'searchable', 'type', 'aggregatable'], field)
pick(['name', 'searchable', 'type', 'aggregatable', 'esTypes', 'subType'], field)
),
title,
}

View file

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

View file

@ -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<AlertsTableComponentProps> = ({
const [showClearSelectionAction, setShowClearSelectionAction] = useState(false);
const [filterGroup, setFilterGroup] = useState<Status>(FILTER_OPEN);
const [shouldShowAddExceptionModal, setShouldShowAddExceptionModal] = useState(false);
const [addExceptionModalState, setAddExceptionModalState] = useState<AddExceptionOnClick>(
const [addExceptionModalState, setAddExceptionModalState] = useState<AddExceptionModalBaseProps>(
addExceptionModalInitialState
);
const [{ browserFields, indexPatterns }] = useFetchIndexPatterns(
@ -216,12 +217,19 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
);
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<AlertsTableComponentProps> = ({
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<AlertsTableComponentProps> = ({
<AddExceptionModal
ruleName={addExceptionModalState.ruleName}
ruleId={addExceptionModalState.ruleId}
ruleIndices={addExceptionModalState.ruleIndices}
exceptionListType={addExceptionModalState.exceptionListType}
alertData={addExceptionModalState.alertData}
onCancel={onAddExceptionCancel}

View file

@ -81,7 +81,7 @@ import { SecurityPageName } from '../../../../../app/types';
import { LinkButton } from '../../../../../common/components/links';
import { useFormatUrl } from '../../../../../common/components/link_to';
import { ExceptionsViewer } from '../../../../../common/components/exceptions/viewer';
import { FILTERS_GLOBAL_HEIGHT } from '../../../../../../common/constants';
import { DEFAULT_INDEX_PATTERN, FILTERS_GLOBAL_HEIGHT } from '../../../../../../common/constants';
import { useFullScreen } from '../../../../../common/containers/use_full_screen';
import { Display } from '../../../../../hosts/pages/display';
import { ExceptionListTypeEnum, ExceptionIdentifiers } from '../../../../../lists_plugin_deps';
@ -515,6 +515,7 @@ export const RuleDetailsPageComponent: FC<PropsFromRedux> = ({
<ExceptionsViewer
ruleId={ruleId ?? ''}
ruleName={rule?.name ?? ''}
ruleIndices={rule?.index ?? DEFAULT_INDEX_PATTERN}
availableListTypes={exceptionLists.allowedExceptionListTypes}
commentsAccordionId={'ruleDetailsTabExceptions'}
exceptionListsMeta={exceptionLists.lists}

View file

@ -2669,6 +2669,22 @@
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "esTypes",
"description": "the elastic type as mapped in the index",
"args": [],
"type": { "kind": "SCALAR", "name": "ToStringArrayNoNullable", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "subType",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "ToIFieldSubTypeNonNullable", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
@ -2676,6 +2692,26 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "ToStringArrayNoNullable",
"description": "",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "ToIFieldSubTypeNonNullable",
"description": "",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "TimerangeInput",

View file

@ -428,6 +428,10 @@ export enum FlowDirection {
biDirectional = 'biDirectional',
}
export type ToStringArrayNoNullable = any;
export type ToIFieldSubTypeNonNullable = any;
export type ToStringArray = string[];
export type Date = string;
@ -627,6 +631,10 @@ export interface IndexField {
description?: Maybe<string>;
format?: Maybe<string>;
/** the elastic type as mapped in the index */
esTypes?: Maybe<ToStringArrayNoNullable>;
subType?: Maybe<ToIFieldSubTypeNonNullable>;
}
export interface AuthenticationsData {
@ -2780,6 +2788,10 @@ export namespace SourceQuery {
aggregatable: boolean;
format: Maybe<string>;
esTypes: Maybe<ToStringArrayNoNullable>;
subType: Maybe<ToIFieldSubTypeNonNullable>;
};
}

View file

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

View file

@ -8,6 +8,7 @@ import gql from 'graphql-tag';
export const ecsSchema = gql`
scalar ToStringArray
scalar ToStringArrayNoNullable
type EventEcsFields {
action: ToStringArray

View file

@ -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<SourceStatusResolvers.IndicesExistResolver>,
@ -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,
});

View file

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

View file

@ -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<string>;
format?: Maybe<string>;
/** the elastic type as mapped in the index */
esTypes?: Maybe<ToStringArrayNoNullable>;
subType?: Maybe<ToIFieldSubTypeNonNullable>;
}
export interface AuthenticationsData {
@ -3579,6 +3587,10 @@ export namespace IndexFieldResolvers {
description?: DescriptionResolver<Maybe<string>, TypeParent, TContext>;
format?: FormatResolver<Maybe<string>, TypeParent, TContext>;
/** the elastic type as mapped in the index */
esTypes?: EsTypesResolver<Maybe<ToStringArrayNoNullable>, TypeParent, TContext>;
subType?: SubTypeResolver<Maybe<ToIFieldSubTypeNonNullable>, TypeParent, TContext>;
}
export type CategoryResolver<R = string, Parent = IndexField, TContext = SiemContext> = Resolver<
@ -3626,6 +3638,16 @@ export namespace IndexFieldResolvers {
Parent = IndexField,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type EsTypesResolver<
R = Maybe<ToStringArrayNoNullable>,
Parent = IndexField,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type SubTypeResolver<
R = Maybe<ToIFieldSubTypeNonNullable>,
Parent = IndexField,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
}
export namespace AuthenticationsDataResolvers {
@ -9317,6 +9339,14 @@ export interface DeprecatedDirectiveArgs {
reason?: string;
}
export interface ToStringArrayNoNullableScalarConfig
extends GraphQLScalarTypeConfig<ToStringArrayNoNullable, any> {
name: 'ToStringArrayNoNullable';
}
export interface ToIFieldSubTypeNonNullableScalarConfig
extends GraphQLScalarTypeConfig<ToIFieldSubTypeNonNullable, any> {
name: 'ToIFieldSubTypeNonNullable';
}
export interface ToStringArrayScalarConfig extends GraphQLScalarTypeConfig<ToStringArray, any> {
name: 'ToStringArray';
}
@ -9490,6 +9520,8 @@ export type IResolvers<TContext = SiemContext> = {
EventsTimelineData?: EventsTimelineDataResolvers.Resolvers<TContext>;
OsFields?: OsFieldsResolvers.Resolvers<TContext>;
HostFields?: HostFieldsResolvers.Resolvers<TContext>;
ToStringArrayNoNullable?: GraphQLScalarType;
ToIFieldSubTypeNonNullable?: GraphQLScalarType;
ToStringArray?: GraphQLScalarType;
Date?: GraphQLScalarType;
ToNumberArray?: GraphQLScalarType;

View file

@ -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<IndexField[]>;
@ -16,4 +17,6 @@ export interface IndexFieldDescriptor {
type: string;
searchable: boolean;
aggregatable: boolean;
esTypes?: string[];
subType?: IFieldSubType;
}