mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution][Exceptions] - Moves remaining exceptions builder logic into lists plugin (#95266)
## Summary Moves part of the exceptions UI out of the security solution plugin and into the lists plugin. In order to keep PRs (relatively) small, I am moving single components at a time. This should also then help more easily pinpoint the source of any issues that come up along the way. The next couple PRs will focus on the exception builder. This one in particular is focused on moving over the `ExceptionBuilderComponent` which deals with rendering numerous exception items and their entries. Quick Summary: - `x-pack/plugins/security_solution/public/common/components/exceptions/builder/` → ` x-pack/plugins/lists/public/exceptions/components/builder/` - Corresponding unit test file moved as well - Updated security solution exception builder to pull `ExceptionBuilderComponent` from lists plugin
This commit is contained in:
parent
7f97f8bc59
commit
92b9482875
28 changed files with 824 additions and 1029 deletions
|
@ -46,7 +46,7 @@ pageLoadAssetSize:
|
|||
lens: 96624
|
||||
licenseManagement: 41817
|
||||
licensing: 29004
|
||||
lists: 202261
|
||||
lists: 228500
|
||||
logstash: 53548
|
||||
management: 46112
|
||||
maps: 80000
|
||||
|
@ -68,7 +68,7 @@ pageLoadAssetSize:
|
|||
searchprofiler: 67080
|
||||
security: 189428
|
||||
securityOss: 30806
|
||||
securitySolution: 283440
|
||||
securitySolution: 235402
|
||||
share: 99061
|
||||
snapshotRestore: 79032
|
||||
spaces: 387915
|
||||
|
|
|
@ -35,13 +35,13 @@ describe(`enumeratePatterns`, () => {
|
|||
'src/plugins/charts/public/static/color_maps/color_maps.ts kibana-app'
|
||||
);
|
||||
});
|
||||
it(`should resolve x-pack/plugins/security_solution/public/common/components/exceptions/builder/translations.ts to kibana-security`, () => {
|
||||
it(`should resolve x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/translations.ts to kibana-security`, () => {
|
||||
const short = 'x-pack/plugins/security_solution';
|
||||
const actual = enumeratePatterns(REPO_ROOT)(log)(new Map([[short, ['kibana-security']]]));
|
||||
|
||||
expect(
|
||||
actual[0].includes(
|
||||
`${short}/public/common/components/exceptions/builder/translations.ts kibana-security`
|
||||
`${short}/public/common/components/exceptions/edit_exception_modal/translations.ts kibana-security`
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,358 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Story, addDecorator } from '@storybook/react';
|
||||
import React from 'react';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import { HttpStart } from 'kibana/public';
|
||||
|
||||
import { AutocompleteStart } from '../../../../../../../src/plugins/data/public';
|
||||
import {
|
||||
fields,
|
||||
getField,
|
||||
} from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
|
||||
import { getMockTheme } from '../../../common/test_utils/kibana_react.mock';
|
||||
import { getEntryMatchAnyMock } from '../../../../common/schemas/types/entry_match_any.mock';
|
||||
import { getEntryMatchMock } from '../../../../common/schemas/types/entry_match.mock';
|
||||
import { getEntryExistsMock } from '../../../../common/schemas/types/entry_exists.mock';
|
||||
import { getEntryNestedMock } from '../../../../common/schemas/types/entry_nested.mock';
|
||||
import { getExceptionListItemSchemaMock } from '../../../../common/schemas/response/exception_list_item_schema.mock';
|
||||
|
||||
import {
|
||||
ExceptionBuilderComponent,
|
||||
ExceptionBuilderProps,
|
||||
OnChangeProps,
|
||||
} from './exception_items_renderer';
|
||||
|
||||
const mockTheme = getMockTheme({
|
||||
darkMode: false,
|
||||
eui: euiLightVars,
|
||||
});
|
||||
const mockHttpService: HttpStart = ({
|
||||
addLoadingCountSource: (): void => {},
|
||||
anonymousPaths: {
|
||||
isAnonymous: (): void => {},
|
||||
register: (): void => {},
|
||||
},
|
||||
basePath: {},
|
||||
delete: (): void => {},
|
||||
externalUrl: {
|
||||
validateUrl: (): void => {},
|
||||
},
|
||||
fetch: (): void => {},
|
||||
get: (): void => {},
|
||||
getLoadingCount$: (): void => {},
|
||||
head: (): void => {},
|
||||
intercept: (): void => {},
|
||||
options: (): void => {},
|
||||
patch: (): void => {},
|
||||
post: (): void => {},
|
||||
put: (): void => {},
|
||||
} as unknown) as HttpStart;
|
||||
const mockAutocompleteService = ({
|
||||
getValueSuggestions: () =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve([
|
||||
'siem-kibana',
|
||||
'win2019-endpoint-mr-pedro',
|
||||
'rock01',
|
||||
'windows-endpoint',
|
||||
'siem-windows',
|
||||
'mothra',
|
||||
]);
|
||||
}, 300);
|
||||
}),
|
||||
} as unknown) as AutocompleteStart;
|
||||
|
||||
addDecorator((storyFn) => <ThemeProvider theme={mockTheme}>{storyFn()}</ThemeProvider>);
|
||||
|
||||
export default {
|
||||
argTypes: {
|
||||
allowLargeValueLists: {
|
||||
control: {
|
||||
type: 'boolean',
|
||||
},
|
||||
description: '`boolean` - set to true to allow large value lists.',
|
||||
table: {
|
||||
defaultValue: {
|
||||
summary: true,
|
||||
},
|
||||
},
|
||||
type: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
autocompleteService: {
|
||||
control: {
|
||||
type: 'object',
|
||||
},
|
||||
description:
|
||||
'`AutocompleteStart` - Kibana data plugin autocomplete service used for field value autocomplete.',
|
||||
type: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
exceptionListItems: {
|
||||
control: {
|
||||
type: 'array',
|
||||
},
|
||||
description:
|
||||
'`ExceptionsBuilderExceptionItem[]` - Any existing exception items - would be populated when editing an exception item.',
|
||||
type: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
httpService: {
|
||||
control: {
|
||||
type: 'object',
|
||||
},
|
||||
description: '`HttpStart` - Kibana service.',
|
||||
type: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
indexPatterns: {
|
||||
description:
|
||||
'`IIndexPattern` - index patterns used to populate field options and value autocomplete.',
|
||||
type: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
isAndDisabled: {
|
||||
control: {
|
||||
type: 'boolean',
|
||||
},
|
||||
description:
|
||||
'`boolean` - set to true to disallow users from using the `AND` button to add entries.',
|
||||
table: {
|
||||
defaultValue: {
|
||||
summary: false,
|
||||
},
|
||||
},
|
||||
type: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
isNestedDisabled: {
|
||||
control: {
|
||||
type: 'boolean',
|
||||
},
|
||||
description:
|
||||
'`boolean` - set to true to disallow users from using the `Add nested` button to add nested entries.',
|
||||
table: {
|
||||
defaultValue: {
|
||||
summary: false,
|
||||
},
|
||||
},
|
||||
type: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
isOrDisabled: {
|
||||
control: {
|
||||
type: 'boolean',
|
||||
},
|
||||
description:
|
||||
'`boolean` - set to true to disallow users from using the `OR` button to add multiple exception items within the builder.',
|
||||
table: {
|
||||
defaultValue: {
|
||||
summary: false,
|
||||
},
|
||||
},
|
||||
type: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
listId: {
|
||||
control: {
|
||||
type: 'string',
|
||||
},
|
||||
description: '`string` - the exception list id.',
|
||||
type: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
listNamespaceType: {
|
||||
control: {
|
||||
options: ['agnostic', 'single'],
|
||||
type: 'select',
|
||||
},
|
||||
description: '`NamespaceType` - Determines whether the list is global or space specific.',
|
||||
type: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
listType: {
|
||||
control: {
|
||||
options: ['detection', 'endpoint'],
|
||||
type: 'select',
|
||||
},
|
||||
description:
|
||||
'`ExceptionListType` - Depending on the list type, certain validations may apply.',
|
||||
type: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
listTypeSpecificIndexPatternFilter: {
|
||||
description:
|
||||
'`(pattern: IIndexPattern, type: ExceptionListType) => IIndexPattern` - callback invoked when index patterns filtered. Optional to be used if you would only like certain fields displayed.',
|
||||
type: {
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
onChange: {
|
||||
description:
|
||||
'`(arg: OnChangeProps) => void` - callback invoked any time builder update to propagate changes up to parent.',
|
||||
type: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
ruleName: {
|
||||
description: '`string` - name of the rule list is associated with.',
|
||||
type: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
component: ExceptionBuilderComponent,
|
||||
title: 'ExceptionBuilderComponent',
|
||||
};
|
||||
|
||||
const BuilderTemplate: Story<ExceptionBuilderProps> = (args) => (
|
||||
<ExceptionBuilderComponent {...args} />
|
||||
);
|
||||
|
||||
export const Default = BuilderTemplate.bind({});
|
||||
Default.args = {
|
||||
allowLargeValueLists: true,
|
||||
autocompleteService: mockAutocompleteService,
|
||||
exceptionListItems: [],
|
||||
httpService: mockHttpService,
|
||||
indexPatterns: { fields, id: '1234', title: 'logstash-*' },
|
||||
isAndDisabled: false,
|
||||
isNestedDisabled: false,
|
||||
isOrDisabled: false,
|
||||
listId: '1234',
|
||||
listNamespaceType: 'single',
|
||||
listType: 'detection',
|
||||
onChange: (): OnChangeProps => ({
|
||||
errorExists: false,
|
||||
exceptionItems: [],
|
||||
exceptionsToDelete: [],
|
||||
}),
|
||||
ruleName: 'My awesome rule',
|
||||
};
|
||||
|
||||
const sampleExceptionItem = {
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [
|
||||
{ ...getEntryMatchAnyMock(), field: getField('ip').name, value: ['some ip'] },
|
||||
{ ...getEntryMatchMock(), field: getField('ssl').name, value: 'false' },
|
||||
{ ...getEntryExistsMock(), field: getField('@timestamp').name },
|
||||
],
|
||||
};
|
||||
|
||||
const sampleNestedExceptionItem = {
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [
|
||||
{ ...getEntryMatchAnyMock(), field: getField('ip').name, value: ['some ip'] },
|
||||
{
|
||||
...getEntryNestedMock(),
|
||||
entries: [
|
||||
{
|
||||
...getEntryMatchMock(),
|
||||
field: 'child',
|
||||
value: 'some nested value',
|
||||
},
|
||||
],
|
||||
field: 'nestedField',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const BuilderSingleExceptionItem: Story<ExceptionBuilderProps> = (args) => (
|
||||
<ExceptionBuilderComponent {...args} />
|
||||
);
|
||||
|
||||
export const SingleExceptionItem = BuilderSingleExceptionItem.bind({});
|
||||
SingleExceptionItem.args = {
|
||||
allowLargeValueLists: true,
|
||||
autocompleteService: mockAutocompleteService,
|
||||
exceptionListItems: [sampleExceptionItem],
|
||||
httpService: mockHttpService,
|
||||
indexPatterns: { fields, id: '1234', title: 'logstash-*' },
|
||||
isAndDisabled: false,
|
||||
isNestedDisabled: false,
|
||||
isOrDisabled: false,
|
||||
listId: '1234',
|
||||
listNamespaceType: 'single',
|
||||
listType: 'detection',
|
||||
onChange: (): OnChangeProps => ({
|
||||
errorExists: false,
|
||||
exceptionItems: [sampleExceptionItem],
|
||||
exceptionsToDelete: [],
|
||||
}),
|
||||
ruleName: 'My awesome rule',
|
||||
};
|
||||
|
||||
const BuilderMultiExceptionItems: Story<ExceptionBuilderProps> = (args) => (
|
||||
<ExceptionBuilderComponent {...args} />
|
||||
);
|
||||
|
||||
export const MultiExceptionItems = BuilderMultiExceptionItems.bind({});
|
||||
MultiExceptionItems.args = {
|
||||
allowLargeValueLists: true,
|
||||
autocompleteService: mockAutocompleteService,
|
||||
exceptionListItems: [sampleExceptionItem, sampleExceptionItem],
|
||||
httpService: mockHttpService,
|
||||
indexPatterns: { fields, id: '1234', title: 'logstash-*' },
|
||||
isAndDisabled: false,
|
||||
isNestedDisabled: false,
|
||||
isOrDisabled: false,
|
||||
listId: '1234',
|
||||
listNamespaceType: 'single',
|
||||
listType: 'detection',
|
||||
onChange: (): OnChangeProps => ({
|
||||
errorExists: false,
|
||||
exceptionItems: [sampleExceptionItem, sampleExceptionItem],
|
||||
exceptionsToDelete: [],
|
||||
}),
|
||||
ruleName: 'My awesome rule',
|
||||
};
|
||||
|
||||
const BuilderWithNested: Story<ExceptionBuilderProps> = (args) => (
|
||||
<ExceptionBuilderComponent {...args} />
|
||||
);
|
||||
|
||||
export const WithNestedExceptionItem = BuilderWithNested.bind({});
|
||||
WithNestedExceptionItem.args = {
|
||||
allowLargeValueLists: true,
|
||||
autocompleteService: mockAutocompleteService,
|
||||
exceptionListItems: [sampleNestedExceptionItem, sampleExceptionItem],
|
||||
httpService: mockHttpService,
|
||||
indexPatterns: { fields, id: '1234', title: 'logstash-*' },
|
||||
isAndDisabled: false,
|
||||
isNestedDisabled: false,
|
||||
isOrDisabled: false,
|
||||
listId: '1234',
|
||||
listNamespaceType: 'single',
|
||||
listType: 'detection',
|
||||
onChange: (): OnChangeProps => ({
|
||||
errorExists: false,
|
||||
exceptionItems: [sampleNestedExceptionItem, sampleExceptionItem],
|
||||
exceptionsToDelete: [],
|
||||
}),
|
||||
ruleName: 'My awesome rule',
|
||||
};
|
|
@ -46,7 +46,10 @@ export interface EntryItemProps {
|
|||
httpService: HttpStart;
|
||||
indexPattern: IIndexPattern;
|
||||
listType: ExceptionListType;
|
||||
listTypeSpecificFilter?: (pattern: IIndexPattern, type: ExceptionListType) => IIndexPattern;
|
||||
listTypeSpecificIndexPatternFilter?: (
|
||||
pattern: IIndexPattern,
|
||||
type: ExceptionListType
|
||||
) => IIndexPattern;
|
||||
onChange: (arg: BuilderEntry, i: number) => void;
|
||||
onlyShowListOperators?: boolean;
|
||||
setErrorsExist: (arg: boolean) => void;
|
||||
|
@ -60,7 +63,7 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
|
|||
httpService,
|
||||
indexPattern,
|
||||
listType,
|
||||
listTypeSpecificFilter,
|
||||
listTypeSpecificIndexPatternFilter,
|
||||
onChange,
|
||||
onlyShowListOperators = false,
|
||||
setErrorsExist,
|
||||
|
@ -123,7 +126,7 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
|
|||
indexPattern,
|
||||
entry,
|
||||
listType,
|
||||
listTypeSpecificFilter
|
||||
listTypeSpecificIndexPatternFilter
|
||||
);
|
||||
const comboBox = (
|
||||
<FieldComponent
|
||||
|
@ -157,7 +160,7 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
|
|||
);
|
||||
}
|
||||
},
|
||||
[indexPattern, entry, listType, listTypeSpecificFilter, handleFieldChange]
|
||||
[indexPattern, entry, listType, listTypeSpecificIndexPatternFilter, handleFieldChange]
|
||||
);
|
||||
|
||||
const renderOperatorInput = (isFirst: boolean): JSX.Element => {
|
||||
|
|
|
@ -45,6 +45,10 @@ interface BuilderExceptionListItemProps {
|
|||
andLogicIncluded: boolean;
|
||||
isOnlyItem: boolean;
|
||||
listType: ExceptionListType;
|
||||
listTypeSpecificIndexPatternFilter?: (
|
||||
pattern: IIndexPattern,
|
||||
type: ExceptionListType
|
||||
) => IIndexPattern;
|
||||
onDeleteExceptionItem: (item: ExceptionsBuilderExceptionItem, index: number) => void;
|
||||
onChangeExceptionItem: (item: ExceptionsBuilderExceptionItem, index: number) => void;
|
||||
setErrorsExist: (arg: boolean) => void;
|
||||
|
@ -61,6 +65,7 @@ export const BuilderExceptionListItemComponent = React.memo<BuilderExceptionList
|
|||
indexPattern,
|
||||
isOnlyItem,
|
||||
listType,
|
||||
listTypeSpecificIndexPatternFilter,
|
||||
andLogicIncluded,
|
||||
onDeleteExceptionItem,
|
||||
onChangeExceptionItem,
|
||||
|
@ -124,24 +129,25 @@ export const BuilderExceptionListItemComponent = React.memo<BuilderExceptionList
|
|||
<MyOverflowContainer grow={1}>
|
||||
<BuilderEntryItem
|
||||
allowLargeValueLists={allowLargeValueLists}
|
||||
httpService={httpService}
|
||||
autocompleteService={autocompleteService}
|
||||
entry={item}
|
||||
httpService={httpService}
|
||||
indexPattern={indexPattern}
|
||||
listType={listType}
|
||||
listTypeSpecificIndexPatternFilter={listTypeSpecificIndexPatternFilter}
|
||||
onChange={handleEntryChange}
|
||||
onlyShowListOperators={onlyShowListOperators}
|
||||
setErrorsExist={setErrorsExist}
|
||||
showLabel={
|
||||
exceptionItemIndex === 0 && index === 0 && item.nested !== 'child'
|
||||
}
|
||||
onChange={handleEntryChange}
|
||||
setErrorsExist={setErrorsExist}
|
||||
onlyShowListOperators={onlyShowListOperators}
|
||||
/>
|
||||
</MyOverflowContainer>
|
||||
<BuilderEntryDeleteButtonComponent
|
||||
entries={exceptionItem.entries}
|
||||
isOnlyItem={isOnlyItem}
|
||||
entryIndex={item.entryIndex}
|
||||
exceptionItemIndex={exceptionItemIndex}
|
||||
isOnlyItem={isOnlyItem}
|
||||
nestedParentIndex={item.parent != null ? item.parent.parentIndex : null}
|
||||
onDelete={handleDeleteEntry}
|
||||
/>
|
||||
|
|
|
@ -9,20 +9,19 @@ import React from 'react';
|
|||
import { ThemeProvider } from 'styled-components';
|
||||
import { ReactWrapper, mount } from 'enzyme';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { coreMock } from 'src/core/public/mocks';
|
||||
import { dataPluginMock } from 'src/plugins/data/public/mocks';
|
||||
|
||||
import {
|
||||
fields,
|
||||
getField,
|
||||
} from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
|
||||
import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
|
||||
import { getEntryMatchAnyMock } from '../../../../../../lists/common/schemas/types/entry_match_any.mock';
|
||||
} from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
|
||||
import { getExceptionListItemSchemaMock } from '../../../../common/schemas/response/exception_list_item_schema.mock';
|
||||
import { getEntryMatchAnyMock } from '../../../../common/schemas/types/entry_match_any.mock';
|
||||
import { getMockTheme } from '../../../common/test_utils/kibana_react.mock';
|
||||
import { getEmptyValue } from '../../../common/empty_value';
|
||||
|
||||
import { getEmptyValue } from '../../empty_value';
|
||||
|
||||
import { ExceptionBuilderComponent } from './';
|
||||
import { getMockTheme } from '../../../lib/kibana/kibana_react.mock';
|
||||
import { coreMock } from 'src/core/public/mocks';
|
||||
import { dataPluginMock } from 'src/plugins/data/public/mocks';
|
||||
import { ExceptionBuilderComponent } from './exception_items_renderer';
|
||||
|
||||
const mockTheme = getMockTheme({
|
||||
eui: {
|
||||
|
@ -47,21 +46,22 @@ describe('ExceptionBuilderComponent', () => {
|
|||
wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionBuilderComponent
|
||||
httpService={mockKibanaHttpService}
|
||||
allowLargeValueLists={true}
|
||||
autocompleteService={autocompleteStartMock}
|
||||
exceptionListItems={[]}
|
||||
listType="detection"
|
||||
listId="list_id"
|
||||
listNamespaceType="single"
|
||||
ruleName="Test rule"
|
||||
httpService={mockKibanaHttpService}
|
||||
indexPatterns={{
|
||||
fields,
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
isOrDisabled={false}
|
||||
isAndDisabled={false}
|
||||
isNestedDisabled={false}
|
||||
isOrDisabled={false}
|
||||
listId="list_id"
|
||||
listNamespaceType="single"
|
||||
listType="detection"
|
||||
ruleName="Test rule"
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
|
@ -85,7 +85,7 @@ describe('ExceptionBuilderComponent', () => {
|
|||
wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionBuilderComponent
|
||||
httpService={mockKibanaHttpService}
|
||||
allowLargeValueLists={true}
|
||||
autocompleteService={autocompleteStartMock}
|
||||
exceptionListItems={[
|
||||
{
|
||||
|
@ -95,18 +95,19 @@ describe('ExceptionBuilderComponent', () => {
|
|||
],
|
||||
},
|
||||
]}
|
||||
listType="detection"
|
||||
listId="list_id"
|
||||
listNamespaceType="single"
|
||||
ruleName="Test rule"
|
||||
httpService={mockKibanaHttpService}
|
||||
indexPatterns={{
|
||||
fields,
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
isOrDisabled={false}
|
||||
isAndDisabled={false}
|
||||
isNestedDisabled={false}
|
||||
isOrDisabled={false}
|
||||
listId="list_id"
|
||||
listNamespaceType="single"
|
||||
listType="detection"
|
||||
ruleName="Test rule"
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
|
@ -129,21 +130,23 @@ describe('ExceptionBuilderComponent', () => {
|
|||
wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionBuilderComponent
|
||||
httpService={mockKibanaHttpService}
|
||||
allowLargeValueLists={true}
|
||||
autocompleteService={autocompleteStartMock}
|
||||
exceptionListItems={[]}
|
||||
listType="detection"
|
||||
listId="list_id"
|
||||
listNamespaceType="single"
|
||||
ruleName="Test rule"
|
||||
httpService={mockKibanaHttpService}
|
||||
indexPatterns={{
|
||||
fields,
|
||||
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
isOrDisabled={false}
|
||||
isAndDisabled={false}
|
||||
isNestedDisabled={false}
|
||||
isOrDisabled={false}
|
||||
listId="list_id"
|
||||
listType="detection"
|
||||
listNamespaceType="single"
|
||||
ruleName="Test rule"
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
|
@ -164,21 +167,22 @@ describe('ExceptionBuilderComponent', () => {
|
|||
wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionBuilderComponent
|
||||
httpService={mockKibanaHttpService}
|
||||
allowLargeValueLists={true}
|
||||
autocompleteService={autocompleteStartMock}
|
||||
exceptionListItems={[]}
|
||||
listType="detection"
|
||||
listId="list_id"
|
||||
listNamespaceType="single"
|
||||
ruleName="Test rule"
|
||||
httpService={mockKibanaHttpService}
|
||||
indexPatterns={{
|
||||
fields,
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
isOrDisabled={false}
|
||||
isAndDisabled={false}
|
||||
isNestedDisabled={false}
|
||||
isOrDisabled={false}
|
||||
listId="list_id"
|
||||
listType="detection"
|
||||
listNamespaceType="single"
|
||||
ruleName="Test rule"
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
|
@ -220,21 +224,22 @@ describe('ExceptionBuilderComponent', () => {
|
|||
wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionBuilderComponent
|
||||
httpService={mockKibanaHttpService}
|
||||
allowLargeValueLists={true}
|
||||
autocompleteService={autocompleteStartMock}
|
||||
exceptionListItems={[]}
|
||||
listType="detection"
|
||||
listId="list_id"
|
||||
listNamespaceType="single"
|
||||
ruleName="Test rule"
|
||||
httpService={mockKibanaHttpService}
|
||||
indexPatterns={{
|
||||
fields,
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
isOrDisabled={false}
|
||||
isAndDisabled={false}
|
||||
isNestedDisabled={false}
|
||||
isOrDisabled={false}
|
||||
listId="list_id"
|
||||
listType="detection"
|
||||
listNamespaceType="single"
|
||||
ruleName="Test rule"
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
|
@ -280,7 +285,7 @@ describe('ExceptionBuilderComponent', () => {
|
|||
wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionBuilderComponent
|
||||
httpService={mockKibanaHttpService}
|
||||
allowLargeValueLists={true}
|
||||
autocompleteService={autocompleteStartMock}
|
||||
exceptionListItems={[
|
||||
{
|
||||
|
@ -290,18 +295,19 @@ describe('ExceptionBuilderComponent', () => {
|
|||
],
|
||||
},
|
||||
]}
|
||||
listType="detection"
|
||||
listId="list_id"
|
||||
listNamespaceType="single"
|
||||
ruleName="Test rule"
|
||||
httpService={mockKibanaHttpService}
|
||||
indexPatterns={{
|
||||
fields,
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
isOrDisabled={false}
|
||||
isAndDisabled={false}
|
||||
isNestedDisabled={false}
|
||||
isOrDisabled={false}
|
||||
listId="list_id"
|
||||
listType="detection"
|
||||
listNamespaceType="single"
|
||||
ruleName="Test rule"
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
|
@ -334,21 +340,22 @@ describe('ExceptionBuilderComponent', () => {
|
|||
wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionBuilderComponent
|
||||
httpService={mockKibanaHttpService}
|
||||
allowLargeValueLists={true}
|
||||
autocompleteService={autocompleteStartMock}
|
||||
exceptionListItems={[]}
|
||||
listType="detection"
|
||||
listId="list_id"
|
||||
listNamespaceType="single"
|
||||
ruleName="Test rule"
|
||||
httpService={mockKibanaHttpService}
|
||||
indexPatterns={{
|
||||
fields,
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
isOrDisabled={false}
|
||||
isAndDisabled={false}
|
||||
isNestedDisabled={false}
|
||||
isOrDisabled={false}
|
||||
listId="list_id"
|
||||
listType="detection"
|
||||
listNamespaceType="single"
|
||||
ruleName="Test rule"
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
|
@ -369,21 +376,22 @@ describe('ExceptionBuilderComponent', () => {
|
|||
wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionBuilderComponent
|
||||
httpService={mockKibanaHttpService}
|
||||
allowLargeValueLists={true}
|
||||
autocompleteService={autocompleteStartMock}
|
||||
exceptionListItems={[]}
|
||||
listType="detection"
|
||||
listId="list_id"
|
||||
listNamespaceType="single"
|
||||
ruleName="Test rule"
|
||||
httpService={mockKibanaHttpService}
|
||||
indexPatterns={{
|
||||
fields,
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
isOrDisabled={false}
|
||||
isAndDisabled={false}
|
||||
isNestedDisabled={false}
|
||||
isOrDisabled={false}
|
||||
listId="list_id"
|
||||
listType="detection"
|
||||
listNamespaceType="single"
|
||||
ruleName="Test rule"
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
|
@ -407,21 +415,22 @@ describe('ExceptionBuilderComponent', () => {
|
|||
wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExceptionBuilderComponent
|
||||
httpService={mockKibanaHttpService}
|
||||
allowLargeValueLists={true}
|
||||
autocompleteService={autocompleteStartMock}
|
||||
exceptionListItems={[]}
|
||||
listType="detection"
|
||||
listId="list_id"
|
||||
listNamespaceType="single"
|
||||
ruleName="Test rule"
|
||||
httpService={mockKibanaHttpService}
|
||||
indexPatterns={{
|
||||
fields,
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
}}
|
||||
isOrDisabled={false}
|
||||
isAndDisabled={false}
|
||||
isNestedDisabled={false}
|
||||
isOrDisabled={false}
|
||||
listId="list_id"
|
||||
listType="detection"
|
||||
listNamespaceType="single"
|
||||
ruleName="Test rule"
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
|
@ -8,33 +8,32 @@
|
|||
import React, { useCallback, useEffect, useReducer } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { HttpStart } from 'kibana/public';
|
||||
import { AutocompleteStart } from 'src/plugins/data/public';
|
||||
import { isEqlRule, isThresholdRule } from '../../../../../common/detection_engine/utils';
|
||||
import { addIdToItem } from '../../../../../common';
|
||||
import { Type } from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { IIndexPattern } from '../../../../../../../../src/plugins/data/common';
|
||||
|
||||
import { addIdToItem } from '../../../../common/shared_imports';
|
||||
import { AutocompleteStart, IIndexPattern } from '../../../../../../../src/plugins/data/public';
|
||||
import {
|
||||
BuilderExceptionListItemComponent,
|
||||
ExceptionListItemSchema,
|
||||
NamespaceType,
|
||||
exceptionListItemSchema,
|
||||
OperatorTypeEnum,
|
||||
OperatorEnum,
|
||||
CreateExceptionListItemSchema,
|
||||
ExceptionListItemSchema,
|
||||
ExceptionListType,
|
||||
NamespaceType,
|
||||
OperatorEnum,
|
||||
OperatorTypeEnum,
|
||||
entriesNested,
|
||||
} from '../../../../../public/shared_imports';
|
||||
import { AndOrBadge } from '../../and_or_badge';
|
||||
exceptionListItemSchema,
|
||||
} from '../../../../common';
|
||||
import { AndOrBadge } from '../and_or_badge';
|
||||
|
||||
import { CreateExceptionListItemBuilderSchema, ExceptionsBuilderExceptionItem } from './types';
|
||||
import { BuilderExceptionListItemComponent } from './exception_item_renderer';
|
||||
import { BuilderLogicButtons } from './logic_buttons';
|
||||
import { getNewExceptionItem, filterExceptionItems } from '../helpers';
|
||||
import { ExceptionsBuilderExceptionItem, CreateExceptionListItemBuilderSchema } from '../types';
|
||||
import { State, exceptionsBuilderReducer } from './reducer';
|
||||
import {
|
||||
containsValueListEntry,
|
||||
filterExceptionItems,
|
||||
getDefaultEmptyEntry,
|
||||
getDefaultNestedEmptyEntry,
|
||||
getNewExceptionItem,
|
||||
} from './helpers';
|
||||
|
||||
const MyInvisibleAndBadge = styled(EuiFlexItem)`
|
||||
|
@ -52,77 +51,82 @@ const MyButtonsContainer = styled(EuiFlexItem)`
|
|||
`;
|
||||
|
||||
const initialState: State = {
|
||||
addNested: false,
|
||||
andLogicIncluded: false,
|
||||
disableAnd: false,
|
||||
disableNested: false,
|
||||
disableOr: false,
|
||||
andLogicIncluded: false,
|
||||
addNested: false,
|
||||
errorExists: 0,
|
||||
exceptions: [],
|
||||
exceptionsToDelete: [],
|
||||
errorExists: 0,
|
||||
};
|
||||
|
||||
interface OnChangeProps {
|
||||
export interface OnChangeProps {
|
||||
errorExists: boolean;
|
||||
exceptionItems: Array<ExceptionListItemSchema | CreateExceptionListItemSchema>;
|
||||
exceptionsToDelete: ExceptionListItemSchema[];
|
||||
errorExists: boolean;
|
||||
}
|
||||
|
||||
interface ExceptionBuilderProps {
|
||||
httpService: HttpStart;
|
||||
export interface ExceptionBuilderProps {
|
||||
allowLargeValueLists: boolean;
|
||||
autocompleteService: AutocompleteStart;
|
||||
exceptionListItems: ExceptionsBuilderExceptionItem[];
|
||||
listType: ExceptionListType;
|
||||
listId: string;
|
||||
listNamespaceType: NamespaceType;
|
||||
ruleName: string;
|
||||
httpService: HttpStart;
|
||||
indexPatterns: IIndexPattern;
|
||||
isOrDisabled: boolean;
|
||||
isAndDisabled: boolean;
|
||||
isNestedDisabled: boolean;
|
||||
isOrDisabled: boolean;
|
||||
listId: string;
|
||||
listNamespaceType: NamespaceType;
|
||||
listType: ExceptionListType;
|
||||
listTypeSpecificIndexPatternFilter?: (
|
||||
pattern: IIndexPattern,
|
||||
type: ExceptionListType
|
||||
) => IIndexPattern;
|
||||
onChange: (arg: OnChangeProps) => void;
|
||||
ruleType?: Type;
|
||||
ruleName: string;
|
||||
}
|
||||
|
||||
export const ExceptionBuilderComponent = ({
|
||||
httpService,
|
||||
allowLargeValueLists,
|
||||
autocompleteService,
|
||||
exceptionListItems,
|
||||
listType,
|
||||
listId,
|
||||
listNamespaceType,
|
||||
ruleName,
|
||||
httpService,
|
||||
indexPatterns,
|
||||
isOrDisabled,
|
||||
isAndDisabled,
|
||||
isNestedDisabled,
|
||||
isOrDisabled,
|
||||
listId,
|
||||
listNamespaceType,
|
||||
listType,
|
||||
listTypeSpecificIndexPatternFilter,
|
||||
onChange,
|
||||
ruleType,
|
||||
}: ExceptionBuilderProps) => {
|
||||
ruleName,
|
||||
}: ExceptionBuilderProps): JSX.Element => {
|
||||
const [
|
||||
{
|
||||
exceptions,
|
||||
exceptionsToDelete,
|
||||
addNested,
|
||||
andLogicIncluded,
|
||||
disableAnd,
|
||||
disableNested,
|
||||
disableOr,
|
||||
addNested,
|
||||
errorExists,
|
||||
exceptions,
|
||||
exceptionsToDelete,
|
||||
},
|
||||
dispatch,
|
||||
] = useReducer(exceptionsBuilderReducer(), {
|
||||
...initialState,
|
||||
disableAnd: isAndDisabled,
|
||||
disableOr: isOrDisabled,
|
||||
disableNested: isNestedDisabled,
|
||||
disableOr: isOrDisabled,
|
||||
});
|
||||
|
||||
const setErrorsExist = useCallback(
|
||||
(hasErrors: boolean): void => {
|
||||
dispatch({
|
||||
type: 'setErrorsExist',
|
||||
errorExists: hasErrors,
|
||||
type: 'setErrorsExist',
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
|
@ -131,8 +135,8 @@ export const ExceptionBuilderComponent = ({
|
|||
const setUpdateExceptions = useCallback(
|
||||
(items: ExceptionsBuilderExceptionItem[]): void => {
|
||||
dispatch({
|
||||
type: 'setExceptions',
|
||||
exceptions: items,
|
||||
type: 'setExceptions',
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
|
@ -141,9 +145,9 @@ export const ExceptionBuilderComponent = ({
|
|||
const setDefaultExceptions = useCallback(
|
||||
(item: ExceptionsBuilderExceptionItem): void => {
|
||||
dispatch({
|
||||
type: 'setDefault',
|
||||
initialState,
|
||||
lastException: item,
|
||||
type: 'setDefault',
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
|
@ -152,8 +156,8 @@ export const ExceptionBuilderComponent = ({
|
|||
const setUpdateExceptionsToDelete = useCallback(
|
||||
(items: ExceptionListItemSchema[]): void => {
|
||||
dispatch({
|
||||
type: 'setExceptionsToDelete',
|
||||
exceptions: items,
|
||||
type: 'setExceptionsToDelete',
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
|
@ -162,8 +166,8 @@ export const ExceptionBuilderComponent = ({
|
|||
const setUpdateAndDisabled = useCallback(
|
||||
(shouldDisable: boolean): void => {
|
||||
dispatch({
|
||||
type: 'setDisableAnd',
|
||||
shouldDisable,
|
||||
type: 'setDisableAnd',
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
|
@ -172,8 +176,8 @@ export const ExceptionBuilderComponent = ({
|
|||
const setUpdateOrDisabled = useCallback(
|
||||
(shouldDisable: boolean): void => {
|
||||
dispatch({
|
||||
type: 'setDisableOr',
|
||||
shouldDisable,
|
||||
type: 'setDisableOr',
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
|
@ -182,8 +186,9 @@ export const ExceptionBuilderComponent = ({
|
|||
const setUpdateAddNested = useCallback(
|
||||
(shouldAddNested: boolean): void => {
|
||||
dispatch({
|
||||
type: 'setAddNested',
|
||||
addNested: shouldAddNested,
|
||||
|
||||
type: 'setAddNested',
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
|
@ -295,8 +300,8 @@ export const ExceptionBuilderComponent = ({
|
|||
...lastEntry.entries,
|
||||
addIdToItem({
|
||||
field: '',
|
||||
type: OperatorTypeEnum.MATCH,
|
||||
operator: OperatorEnum.INCLUDED,
|
||||
type: OperatorTypeEnum.MATCH,
|
||||
value: '',
|
||||
}),
|
||||
],
|
||||
|
@ -331,9 +336,9 @@ export const ExceptionBuilderComponent = ({
|
|||
// Bubble up changes to parent
|
||||
useEffect(() => {
|
||||
onChange({
|
||||
errorExists: errorExists > 0,
|
||||
exceptionItems: filterExceptionItems(exceptions),
|
||||
exceptionsToDelete,
|
||||
errorExists: errorExists > 0,
|
||||
});
|
||||
}, [onChange, exceptionsToDelete, exceptions, errorExists]);
|
||||
|
||||
|
@ -381,18 +386,19 @@ export const ExceptionBuilderComponent = ({
|
|||
))}
|
||||
<EuiFlexItem grow={false}>
|
||||
<BuilderExceptionListItemComponent
|
||||
allowLargeValueLists={!isEqlRule(ruleType) && !isThresholdRule(ruleType)}
|
||||
httpService={httpService}
|
||||
autocompleteService={autocompleteService}
|
||||
key={getExceptionListItemId(exceptionListItem, index)}
|
||||
exceptionItem={exceptionListItem}
|
||||
indexPattern={indexPatterns}
|
||||
listType={listType}
|
||||
exceptionItemIndex={index}
|
||||
allowLargeValueLists={allowLargeValueLists}
|
||||
andLogicIncluded={andLogicIncluded}
|
||||
autocompleteService={autocompleteService}
|
||||
exceptionItem={exceptionListItem}
|
||||
exceptionItemIndex={index}
|
||||
httpService={httpService}
|
||||
indexPattern={indexPatterns}
|
||||
isOnlyItem={exceptions.length === 1}
|
||||
onDeleteExceptionItem={handleDeleteExceptionItem}
|
||||
key={getExceptionListItemId(exceptionListItem, index)}
|
||||
listType={listType}
|
||||
listTypeSpecificIndexPatternFilter={listTypeSpecificIndexPatternFilter}
|
||||
onChangeExceptionItem={handleExceptionItemChange}
|
||||
onDeleteExceptionItem={handleDeleteExceptionItem}
|
||||
onlyShowListOperators={containsValueListEntry(exceptions)}
|
||||
setErrorsExist={setErrorsExist}
|
||||
/>
|
|
@ -5,15 +5,27 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import uuid from 'uuid';
|
||||
|
||||
import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/public';
|
||||
import { addIdToItem } from '../../../../common/shared_imports';
|
||||
import { addIdToItem, removeIdFromItem, validate } from '../../../../common/shared_imports';
|
||||
import {
|
||||
CreateExceptionListItemSchema,
|
||||
EntriesArray,
|
||||
Entry,
|
||||
EntryNested,
|
||||
ExceptionListItemSchema,
|
||||
ExceptionListType,
|
||||
ListSchema,
|
||||
NamespaceType,
|
||||
OperatorEnum,
|
||||
OperatorTypeEnum,
|
||||
createExceptionListItemSchema,
|
||||
entriesList,
|
||||
entriesNested,
|
||||
entry,
|
||||
exceptionListItemSchema,
|
||||
nestedEntryItem,
|
||||
} from '../../../../common';
|
||||
import {
|
||||
EXCEPTION_OPERATORS,
|
||||
|
@ -28,6 +40,8 @@ import { OperatorOption } from '../autocomplete/types';
|
|||
|
||||
import {
|
||||
BuilderEntry,
|
||||
CreateExceptionListItemBuilderSchema,
|
||||
EmptyEntry,
|
||||
EmptyNestedEntry,
|
||||
ExceptionsBuilderExceptionItem,
|
||||
FormattedBuilderEntry,
|
||||
|
@ -37,6 +51,105 @@ export const isEntryNested = (item: BuilderEntry): item is EntryNested => {
|
|||
return (item as EntryNested).entries != null;
|
||||
};
|
||||
|
||||
export const filterExceptionItems = (
|
||||
exceptions: ExceptionsBuilderExceptionItem[]
|
||||
): Array<ExceptionListItemSchema | CreateExceptionListItemSchema> => {
|
||||
return exceptions.reduce<Array<ExceptionListItemSchema | CreateExceptionListItemSchema>>(
|
||||
(acc, exception) => {
|
||||
const entries = exception.entries.reduce<BuilderEntry[]>((nestedAcc, singleEntry) => {
|
||||
const strippedSingleEntry = removeIdFromItem(singleEntry);
|
||||
|
||||
if (entriesNested.is(strippedSingleEntry)) {
|
||||
const nestedEntriesArray = strippedSingleEntry.entries.filter((singleNestedEntry) => {
|
||||
const noIdSingleNestedEntry = removeIdFromItem(singleNestedEntry);
|
||||
const [validatedNestedEntry] = validate(noIdSingleNestedEntry, nestedEntryItem);
|
||||
return validatedNestedEntry != null;
|
||||
});
|
||||
const noIdNestedEntries = nestedEntriesArray.map((singleNestedEntry) =>
|
||||
removeIdFromItem(singleNestedEntry)
|
||||
);
|
||||
|
||||
const [validatedNestedEntry] = validate(
|
||||
{ ...strippedSingleEntry, entries: noIdNestedEntries },
|
||||
entriesNested
|
||||
);
|
||||
|
||||
if (validatedNestedEntry != null) {
|
||||
return [...nestedAcc, { ...singleEntry, entries: nestedEntriesArray }];
|
||||
}
|
||||
return nestedAcc;
|
||||
} else {
|
||||
const [validatedEntry] = validate(strippedSingleEntry, entry);
|
||||
|
||||
if (validatedEntry != null) {
|
||||
return [...nestedAcc, singleEntry];
|
||||
}
|
||||
return nestedAcc;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const item = { ...exception, entries };
|
||||
|
||||
if (exceptionListItemSchema.is(item)) {
|
||||
return [...acc, item];
|
||||
} else if (createExceptionListItemSchema.is(item)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { meta: _, ...rest } = item;
|
||||
const itemSansMetaId: CreateExceptionListItemSchema = { ...rest, meta: undefined };
|
||||
return [...acc, itemSansMetaId];
|
||||
} else {
|
||||
return acc;
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
export const addIdToEntries = (entries: EntriesArray): EntriesArray => {
|
||||
return entries.map((singleEntry) => {
|
||||
if (singleEntry.type === 'nested') {
|
||||
return addIdToItem({
|
||||
...singleEntry,
|
||||
entries: singleEntry.entries.map((nestedEntry) => addIdToItem(nestedEntry)),
|
||||
});
|
||||
} else {
|
||||
return addIdToItem(singleEntry);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const getNewExceptionItem = ({
|
||||
listId,
|
||||
namespaceType,
|
||||
ruleName,
|
||||
}: {
|
||||
listId: string;
|
||||
namespaceType: NamespaceType;
|
||||
ruleName: string;
|
||||
}): CreateExceptionListItemBuilderSchema => {
|
||||
return {
|
||||
comments: [],
|
||||
description: `${ruleName} - exception list item`,
|
||||
entries: addIdToEntries([
|
||||
{
|
||||
field: '',
|
||||
operator: 'included',
|
||||
type: 'match',
|
||||
value: '',
|
||||
},
|
||||
]),
|
||||
item_id: undefined,
|
||||
list_id: listId,
|
||||
meta: {
|
||||
temporaryUuid: uuid.v4(),
|
||||
},
|
||||
name: `${ruleName} - exception list item`,
|
||||
namespace_type: namespaceType,
|
||||
tags: [],
|
||||
type: 'simple',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the operator type, may not need this if using io-ts types
|
||||
*
|
||||
|
@ -665,3 +778,21 @@ export const getFormattedBuilderEntries = (
|
|||
}
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const getDefaultEmptyEntry = (): EmptyEntry => ({
|
||||
field: '',
|
||||
id: uuid.v4(),
|
||||
operator: OperatorEnum.INCLUDED,
|
||||
type: OperatorTypeEnum.MATCH,
|
||||
value: '',
|
||||
});
|
||||
|
||||
export const getDefaultNestedEmptyEntry = (): EmptyNestedEntry => ({
|
||||
entries: [],
|
||||
field: '',
|
||||
id: uuid.v4(),
|
||||
type: OperatorTypeEnum.NESTED,
|
||||
});
|
||||
|
||||
export const containsValueListEntry = (items: ExceptionsBuilderExceptionItem[]): boolean =>
|
||||
items.some((item) => item.entries.some(({ type }) => type === OperatorTypeEnum.LIST));
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { BuilderEntryItem } from './entry_renderer';
|
||||
export { BuilderExceptionListItemComponent } from './exception_item_renderer';
|
||||
export { ExceptionBuilderComponent } from './exception_items_renderer';
|
|
@ -6,38 +6,37 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
|
||||
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import * as i18nShared from '../translations';
|
||||
|
||||
const MyEuiButton = styled(EuiButton)`
|
||||
min-width: 95px;
|
||||
`;
|
||||
|
||||
interface BuilderLogicButtonsProps {
|
||||
isOrDisabled: boolean;
|
||||
isAndDisabled: boolean;
|
||||
isNestedDisabled: boolean;
|
||||
isNested: boolean;
|
||||
isNestedDisabled: boolean;
|
||||
isOrDisabled: boolean;
|
||||
showNestedButton: boolean;
|
||||
onAndClicked: () => void;
|
||||
onOrClicked: () => void;
|
||||
onNestedClicked: () => void;
|
||||
onAddClickWhenNested: () => void;
|
||||
onAndClicked: () => void;
|
||||
onNestedClicked: () => void;
|
||||
onOrClicked: () => void;
|
||||
}
|
||||
|
||||
export const BuilderLogicButtons: React.FC<BuilderLogicButtonsProps> = ({
|
||||
isOrDisabled = false,
|
||||
isAndDisabled = false,
|
||||
showNestedButton = false,
|
||||
isNestedDisabled = true,
|
||||
isNested,
|
||||
onAndClicked,
|
||||
onOrClicked,
|
||||
onNestedClicked,
|
||||
isNestedDisabled = true,
|
||||
isOrDisabled = false,
|
||||
showNestedButton = false,
|
||||
onAddClickWhenNested,
|
||||
onAndClicked,
|
||||
onNestedClicked,
|
||||
onOrClicked,
|
||||
}) => (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -49,7 +48,7 @@ export const BuilderLogicButtons: React.FC<BuilderLogicButtonsProps> = ({
|
|||
data-test-subj="exceptionsAndButton"
|
||||
isDisabled={isAndDisabled}
|
||||
>
|
||||
{i18nShared.AND}
|
||||
{i18n.AND}
|
||||
</MyEuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -61,7 +60,7 @@ export const BuilderLogicButtons: React.FC<BuilderLogicButtonsProps> = ({
|
|||
isDisabled={isOrDisabled}
|
||||
data-test-subj="exceptionsOrButton"
|
||||
>
|
||||
{i18nShared.OR}
|
||||
{i18n.OR}
|
||||
</MyEuiButton>
|
||||
</EuiFlexItem>
|
||||
{showNestedButton && (
|
|
@ -5,8 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ExceptionsBuilderExceptionItem } from '../types';
|
||||
import { ExceptionListItemSchema, OperatorTypeEnum } from '../../../../../public/lists_plugin_deps';
|
||||
import { ExceptionListItemSchema, OperatorTypeEnum } from '../../../../common';
|
||||
|
||||
import { ExceptionsBuilderExceptionItem } from './types';
|
||||
import { getDefaultEmptyEntry } from './helpers';
|
||||
|
||||
export type ViewerModalName = 'addModal' | 'editModal' | null;
|
||||
|
@ -58,7 +59,7 @@ export const exceptionsBuilderReducer = () => (state: State, action: Action): St
|
|||
case 'setExceptions': {
|
||||
const isAndLogicIncluded =
|
||||
action.exceptions.filter(({ entries }) => entries.length > 1).length > 0;
|
||||
const lastExceptionItem = action.exceptions.slice(-1)[0];
|
||||
const [lastExceptionItem] = action.exceptions.slice(-1);
|
||||
const isAddNested =
|
||||
lastExceptionItem != null
|
||||
? lastExceptionItem.entries.slice(-1).filter(({ type }) => type === 'nested').length > 0
|
||||
|
@ -73,12 +74,12 @@ export const exceptionsBuilderReducer = () => (state: State, action: Action): St
|
|||
|
||||
return {
|
||||
...state,
|
||||
andLogicIncluded: isAndLogicIncluded,
|
||||
exceptions: action.exceptions,
|
||||
addNested: isAddNested,
|
||||
andLogicIncluded: isAndLogicIncluded,
|
||||
disableAnd: isAndDisabled,
|
||||
disableOr: isOrDisabled,
|
||||
disableNested: containsValueList,
|
||||
disableOr: isOrDisabled,
|
||||
exceptions: action.exceptions,
|
||||
};
|
||||
}
|
||||
case 'setDefault': {
|
|
@ -53,3 +53,25 @@ export const EXCEPTION_OPERATOR_PLACEHOLDER = i18n.translate(
|
|||
defaultMessage: 'Operator',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_NESTED_DESCRIPTION = i18n.translate(
|
||||
'xpack.lists.exceptions.builder.addNestedDescription',
|
||||
{
|
||||
defaultMessage: 'Add nested condition',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_NON_NESTED_DESCRIPTION = i18n.translate(
|
||||
'xpack.lists.exceptions.builder.addNonNestedDescription',
|
||||
{
|
||||
defaultMessage: 'Add non-nested condition',
|
||||
}
|
||||
);
|
||||
|
||||
export const AND = i18n.translate('xpack.lists.exceptions.andDescription', {
|
||||
defaultMessage: 'AND',
|
||||
});
|
||||
|
||||
export const OR = i18n.translate('xpack.lists.exceptions.orDescription', {
|
||||
defaultMessage: 'OR',
|
||||
});
|
||||
|
|
|
@ -38,7 +38,4 @@ export {
|
|||
UseExceptionListItemsSuccess,
|
||||
UseExceptionListsSuccess,
|
||||
} from './exceptions/types';
|
||||
export { BuilderEntryItem } from './exceptions/components/builder/entry_renderer';
|
||||
export { BuilderAndBadgeComponent } from './exceptions/components/builder/and_badge';
|
||||
export { BuilderEntryDeleteButtonComponent } from './exceptions/components/builder/entry_delete_button';
|
||||
export { BuilderExceptionListItemComponent } from './exceptions/components/builder/exception_item_renderer';
|
||||
export * as ExceptionBuilder from './exceptions/components/builder/index';
|
||||
|
|
|
@ -12,14 +12,13 @@ import { waitFor } from '@testing-library/react';
|
|||
|
||||
import { AddExceptionModal } from './';
|
||||
import { useCurrentUser } from '../../../../common/lib/kibana';
|
||||
import { useAsync } from '../../../../shared_imports';
|
||||
import { useAsync, ExceptionBuilder } from '../../../../shared_imports';
|
||||
import { getExceptionListSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_schema.mock';
|
||||
import { useFetchIndex } from '../../../containers/source';
|
||||
import { stubIndexPattern } from 'src/plugins/data/common/index_patterns/index_pattern.stub';
|
||||
import { useAddOrUpdateException } from '../use_add_exception';
|
||||
import { useFetchOrCreateRuleExceptionList } from '../use_fetch_or_create_rule_exception_list';
|
||||
import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index';
|
||||
import * as builder from '../builder';
|
||||
import * as helpers from '../helpers';
|
||||
import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
|
||||
import { EntriesArray } from '../../../../../../lists/common/schemas/types';
|
||||
|
@ -49,7 +48,6 @@ jest.mock('../../../containers/source');
|
|||
jest.mock('../../../../detections/containers/detection_engine/rules');
|
||||
jest.mock('../use_add_exception');
|
||||
jest.mock('../use_fetch_or_create_rule_exception_list');
|
||||
jest.mock('../builder');
|
||||
jest.mock('../../../../shared_imports');
|
||||
jest.mock('../../../../detections/containers/detection_engine/rules/use_rule_async');
|
||||
|
||||
|
@ -59,12 +57,12 @@ describe('When the add exception modal is opened', () => {
|
|||
ReturnType<typeof helpers.defaultEndpointExceptionItems>
|
||||
>;
|
||||
let ExceptionBuilderComponent: jest.SpyInstance<
|
||||
ReturnType<typeof builder.ExceptionBuilderComponent>
|
||||
ReturnType<typeof ExceptionBuilder.ExceptionBuilderComponent>
|
||||
>;
|
||||
beforeEach(() => {
|
||||
defaultEndpointItems = jest.spyOn(helpers, 'defaultEndpointExceptionItems');
|
||||
ExceptionBuilderComponent = jest
|
||||
.spyOn(builder, 'ExceptionBuilderComponent')
|
||||
.spyOn(ExceptionBuilder, 'ExceptionBuilderComponent')
|
||||
.mockReturnValue(<></>);
|
||||
|
||||
(useAsync as jest.Mock).mockImplementation(() => ({
|
||||
|
|
|
@ -23,19 +23,23 @@ import {
|
|||
EuiText,
|
||||
EuiCallOut,
|
||||
} from '@elastic/eui';
|
||||
import { hasEqlSequenceQuery, isEqlRule } from '../../../../../common/detection_engine/utils';
|
||||
import {
|
||||
hasEqlSequenceQuery,
|
||||
isEqlRule,
|
||||
isThresholdRule,
|
||||
} from '../../../../../common/detection_engine/utils';
|
||||
import { Status } from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
import {
|
||||
ExceptionListItemSchema,
|
||||
CreateExceptionListItemSchema,
|
||||
ExceptionListType,
|
||||
} from '../../../../../public/lists_plugin_deps';
|
||||
ExceptionBuilder,
|
||||
} from '../../../../../public/shared_imports';
|
||||
import * as i18nCommon from '../../../translations';
|
||||
import * as i18n from './translations';
|
||||
import * as sharedI18n from '../translations';
|
||||
import { useAppToasts } from '../../../hooks/use_app_toasts';
|
||||
import { useKibana } from '../../../lib/kibana';
|
||||
import { ExceptionBuilderComponent } from '../builder';
|
||||
import { Loader } from '../../loader';
|
||||
import { useAddOrUpdateException } from '../use_add_exception';
|
||||
import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index';
|
||||
|
@ -50,6 +54,7 @@ import {
|
|||
entryHasListType,
|
||||
entryHasNonEcsType,
|
||||
retrieveAlertOsTypes,
|
||||
filterIndexPatterns,
|
||||
} from '../helpers';
|
||||
import { ErrorInfo, ErrorCallout } from '../error_callout';
|
||||
import { AlertData, ExceptionsBuilderExceptionItem } from '../types';
|
||||
|
@ -393,13 +398,17 @@ export const AddExceptionModal = memo(function AddExceptionModal({
|
|||
)}
|
||||
<EuiText>{i18n.EXCEPTION_BUILDER_INFO}</EuiText>
|
||||
<EuiSpacer />
|
||||
<ExceptionBuilderComponent
|
||||
<ExceptionBuilder.ExceptionBuilderComponent
|
||||
allowLargeValueLists={
|
||||
!isEqlRule(maybeRule?.type) && !isThresholdRule(maybeRule?.type)
|
||||
}
|
||||
httpService={http}
|
||||
autocompleteService={data.autocomplete}
|
||||
exceptionListItems={initialExceptionItems}
|
||||
listType={exceptionListType}
|
||||
listId={ruleExceptionList.list_id}
|
||||
listNamespaceType={ruleExceptionList.namespace_type}
|
||||
listTypeSpecificIndexPatternFilter={filterIndexPatterns}
|
||||
ruleName={ruleName}
|
||||
indexPatterns={indexPatterns}
|
||||
isOrDisabled={false}
|
||||
|
@ -408,7 +417,6 @@ export const AddExceptionModal = memo(function AddExceptionModal({
|
|||
data-test-subj="alert-exception-builder"
|
||||
id-aria="alert-exception-builder"
|
||||
onChange={handleBuilderOnChange}
|
||||
ruleType={maybeRule?.type}
|
||||
/>
|
||||
|
||||
<EuiSpacer />
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { fields } from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
|
||||
import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common';
|
||||
|
||||
import { filterIndexPatterns } from './helpers';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn().mockReturnValue('123'),
|
||||
}));
|
||||
|
||||
const getMockIndexPattern = (): IIndexPattern => ({
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields,
|
||||
});
|
||||
|
||||
const mockEndpointFields = [
|
||||
{
|
||||
name: 'file.path.caseless',
|
||||
type: 'string',
|
||||
esTypes: ['keyword'],
|
||||
count: 0,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: false,
|
||||
readFromDocValues: false,
|
||||
},
|
||||
{
|
||||
name: 'file.Ext.code_signature.status',
|
||||
type: 'string',
|
||||
esTypes: ['text'],
|
||||
count: 0,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: false,
|
||||
readFromDocValues: false,
|
||||
subType: { nested: { path: 'file.Ext.code_signature' } },
|
||||
},
|
||||
];
|
||||
|
||||
export const getEndpointField = (name: string) =>
|
||||
mockEndpointFields.find((field) => field.name === name) as IFieldType;
|
||||
|
||||
describe('Exception builder helpers', () => {
|
||||
describe('#filterIndexPatterns', () => {
|
||||
test('it returns index patterns without filtering if list type is "detection"', () => {
|
||||
const mockIndexPatterns = getMockIndexPattern();
|
||||
const output = filterIndexPatterns(mockIndexPatterns, 'detection');
|
||||
|
||||
expect(output).toEqual(mockIndexPatterns);
|
||||
});
|
||||
|
||||
test('it returns filtered index patterns if list type is "endpoint"', () => {
|
||||
const mockIndexPatterns = {
|
||||
...getMockIndexPattern(),
|
||||
fields: [...fields, ...mockEndpointFields],
|
||||
};
|
||||
const output = filterIndexPatterns(mockIndexPatterns, 'endpoint');
|
||||
|
||||
expect(output).toEqual({ ...getMockIndexPattern(), fields: [...mockEndpointFields] });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import uuid from 'uuid';
|
||||
|
||||
import { IIndexPattern } from '../../../../../../../../src/plugins/data/common';
|
||||
import { OperatorTypeEnum, ExceptionListType, OperatorEnum } from '../../../../lists_plugin_deps';
|
||||
import { ExceptionsBuilderExceptionItem, EmptyEntry, EmptyNestedEntry } from '../types';
|
||||
import exceptionableFields from '../exceptionable_fields.json';
|
||||
|
||||
export const filterIndexPatterns = (
|
||||
patterns: IIndexPattern,
|
||||
type: ExceptionListType
|
||||
): IIndexPattern => {
|
||||
return type === 'endpoint'
|
||||
? {
|
||||
...patterns,
|
||||
fields: patterns.fields.filter(({ name }) => exceptionableFields.includes(name)),
|
||||
}
|
||||
: patterns;
|
||||
};
|
||||
|
||||
export const getDefaultEmptyEntry = (): EmptyEntry => ({
|
||||
id: uuid.v4(),
|
||||
field: '',
|
||||
type: OperatorTypeEnum.MATCH,
|
||||
operator: OperatorEnum.INCLUDED,
|
||||
value: '',
|
||||
});
|
||||
|
||||
export const getDefaultNestedEmptyEntry = (): EmptyNestedEntry => ({
|
||||
id: uuid.v4(),
|
||||
field: '',
|
||||
type: OperatorTypeEnum.NESTED,
|
||||
entries: [],
|
||||
});
|
||||
|
||||
export const containsValueListEntry = (items: ExceptionsBuilderExceptionItem[]): boolean =>
|
||||
items.some((item) => item.entries.some((entry) => entry.type === OperatorTypeEnum.LIST));
|
|
@ -1,110 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { storiesOf, addDecorator } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import React from 'react';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
|
||||
|
||||
import { BuilderLogicButtons } from './logic_buttons';
|
||||
|
||||
addDecorator((storyFn) => (
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>{storyFn()}</ThemeProvider>
|
||||
));
|
||||
|
||||
storiesOf('Exceptions/BuilderLogicButtons', module)
|
||||
.add('and/or buttons', () => {
|
||||
return (
|
||||
<BuilderLogicButtons
|
||||
isAndDisabled={false}
|
||||
isOrDisabled={false}
|
||||
isNestedDisabled={false}
|
||||
isNested={false}
|
||||
showNestedButton={false}
|
||||
onOrClicked={action('onClick')}
|
||||
onAndClicked={action('onClick')}
|
||||
onNestedClicked={action('onClick')}
|
||||
onAddClickWhenNested={action('onClick')}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('nested button - isNested false', () => {
|
||||
return (
|
||||
<BuilderLogicButtons
|
||||
isAndDisabled={false}
|
||||
isOrDisabled={false}
|
||||
isNestedDisabled={false}
|
||||
isNested={false}
|
||||
showNestedButton
|
||||
onOrClicked={action('onClick')}
|
||||
onAndClicked={action('onClick')}
|
||||
onNestedClicked={action('onClick')}
|
||||
onAddClickWhenNested={action('onClick')}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('nested button - isNested true', () => {
|
||||
return (
|
||||
<BuilderLogicButtons
|
||||
isAndDisabled={false}
|
||||
isOrDisabled={false}
|
||||
isNestedDisabled={false}
|
||||
isNested
|
||||
showNestedButton
|
||||
onOrClicked={action('onClick')}
|
||||
onAndClicked={action('onClick')}
|
||||
onNestedClicked={action('onClick')}
|
||||
onAddClickWhenNested={action('onClick')}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('and disabled', () => {
|
||||
return (
|
||||
<BuilderLogicButtons
|
||||
isAndDisabled
|
||||
isOrDisabled={false}
|
||||
isNestedDisabled={false}
|
||||
isNested={false}
|
||||
showNestedButton={false}
|
||||
onOrClicked={action('onClick')}
|
||||
onAndClicked={action('onClick')}
|
||||
onNestedClicked={action('onClick')}
|
||||
onAddClickWhenNested={action('onClick')}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('or disabled', () => {
|
||||
return (
|
||||
<BuilderLogicButtons
|
||||
isAndDisabled={false}
|
||||
isOrDisabled
|
||||
isNestedDisabled={false}
|
||||
isNested={false}
|
||||
showNestedButton={false}
|
||||
onOrClicked={action('onClick')}
|
||||
onAndClicked={action('onClick')}
|
||||
onNestedClicked={action('onClick')}
|
||||
onAddClickWhenNested={action('onClick')}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('nested disabled', () => {
|
||||
return (
|
||||
<BuilderLogicButtons
|
||||
isAndDisabled={false}
|
||||
isOrDisabled={false}
|
||||
isNestedDisabled
|
||||
isNested={false}
|
||||
showNestedButton
|
||||
onOrClicked={action('onClick')}
|
||||
onAndClicked={action('onClick')}
|
||||
onNestedClicked={action('onClick')}
|
||||
onAddClickWhenNested={action('onClick')}
|
||||
/>
|
||||
);
|
||||
});
|
|
@ -1,521 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
|
||||
import { getEntryMatchMock } from '../../../../../../lists/common/schemas/types/entry_match.mock';
|
||||
import { getEntryNestedMock } from '../../../../../../lists/common/schemas/types/entry_nested.mock';
|
||||
import { getEntryListMock } from '../../../../../../lists/common/schemas/types/entry_list.mock';
|
||||
|
||||
import { ExceptionsBuilderExceptionItem } from '../types';
|
||||
import { Action, State, exceptionsBuilderReducer } from './reducer';
|
||||
import { getDefaultEmptyEntry } from './helpers';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn().mockReturnValue('123'),
|
||||
}));
|
||||
|
||||
const initialState: State = {
|
||||
disableAnd: false,
|
||||
disableNested: false,
|
||||
disableOr: false,
|
||||
andLogicIncluded: false,
|
||||
addNested: false,
|
||||
exceptions: [],
|
||||
exceptionsToDelete: [],
|
||||
errorExists: 0,
|
||||
};
|
||||
|
||||
describe('exceptionsBuilderReducer', () => {
|
||||
let reducer: (state: State, action: Action) => State;
|
||||
|
||||
beforeEach(() => {
|
||||
reducer = exceptionsBuilderReducer();
|
||||
});
|
||||
|
||||
describe('#setExceptions', () => {
|
||||
test('should return "andLogicIncluded" ', () => {
|
||||
const update = reducer(initialState, {
|
||||
type: 'setExceptions',
|
||||
exceptions: [],
|
||||
});
|
||||
|
||||
expect(update).toEqual({
|
||||
disableAnd: false,
|
||||
disableNested: false,
|
||||
disableOr: false,
|
||||
andLogicIncluded: false,
|
||||
addNested: false,
|
||||
exceptions: [],
|
||||
exceptionsToDelete: [],
|
||||
errorExists: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test('should set "andLogicIncluded" to true if any of the exceptions include entries with length greater than 1 ', () => {
|
||||
const exceptions: ExceptionsBuilderExceptionItem[] = [
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [getEntryMatchMock(), getEntryMatchMock()],
|
||||
},
|
||||
];
|
||||
const { andLogicIncluded } = reducer(initialState, {
|
||||
type: 'setExceptions',
|
||||
exceptions,
|
||||
});
|
||||
|
||||
expect(andLogicIncluded).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should set "andLogicIncluded" to false if any of the exceptions include entries with length greater than 1 ', () => {
|
||||
const exceptions: ExceptionsBuilderExceptionItem[] = [
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [getEntryMatchMock()],
|
||||
},
|
||||
];
|
||||
const { andLogicIncluded } = reducer(initialState, {
|
||||
type: 'setExceptions',
|
||||
exceptions,
|
||||
});
|
||||
|
||||
expect(andLogicIncluded).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should set "addNested" to true if last exception entry is type nested', () => {
|
||||
const exceptions: ExceptionsBuilderExceptionItem[] = [
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [getEntryMatchMock()],
|
||||
},
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [getEntryMatchMock(), getEntryNestedMock()],
|
||||
},
|
||||
];
|
||||
const { addNested } = reducer(initialState, {
|
||||
type: 'setExceptions',
|
||||
exceptions,
|
||||
});
|
||||
|
||||
expect(addNested).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should set "addNested" to false if last exception item entry is not type nested', () => {
|
||||
const exceptions: ExceptionsBuilderExceptionItem[] = [
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [getEntryMatchMock(), getEntryNestedMock()],
|
||||
},
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [getEntryMatchMock()],
|
||||
},
|
||||
];
|
||||
const { addNested } = reducer(initialState, {
|
||||
type: 'setExceptions',
|
||||
exceptions,
|
||||
});
|
||||
|
||||
expect(addNested).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should set "disableOr" to true if last exception entry is type nested', () => {
|
||||
const exceptions: ExceptionsBuilderExceptionItem[] = [
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [getEntryMatchMock()],
|
||||
},
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [getEntryMatchMock(), getEntryNestedMock()],
|
||||
},
|
||||
];
|
||||
const { disableOr } = reducer(initialState, {
|
||||
type: 'setExceptions',
|
||||
exceptions,
|
||||
});
|
||||
|
||||
expect(disableOr).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should set "disableOr" to false if last exception item entry is not type nested', () => {
|
||||
const exceptions: ExceptionsBuilderExceptionItem[] = [
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [getEntryMatchMock(), getEntryNestedMock()],
|
||||
},
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [getEntryMatchMock()],
|
||||
},
|
||||
];
|
||||
const { disableOr } = reducer(initialState, {
|
||||
type: 'setExceptions',
|
||||
exceptions,
|
||||
});
|
||||
|
||||
expect(disableOr).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should set "disableNested" to true if an exception item includes an entry of type list', () => {
|
||||
const exceptions: ExceptionsBuilderExceptionItem[] = [
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [getEntryListMock()],
|
||||
},
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [getEntryMatchMock(), getEntryNestedMock()],
|
||||
},
|
||||
];
|
||||
const { disableNested } = reducer(initialState, {
|
||||
type: 'setExceptions',
|
||||
exceptions,
|
||||
});
|
||||
|
||||
expect(disableNested).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should set "disableNested" to false if an exception item does not include an entry of type list', () => {
|
||||
const exceptions: ExceptionsBuilderExceptionItem[] = [
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [getEntryMatchMock(), getEntryNestedMock()],
|
||||
},
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [getEntryMatchMock()],
|
||||
},
|
||||
];
|
||||
const { disableNested } = reducer(initialState, {
|
||||
type: 'setExceptions',
|
||||
exceptions,
|
||||
});
|
||||
|
||||
expect(disableNested).toBeFalsy();
|
||||
});
|
||||
|
||||
// What does that even mean?! :) Just checking if a user has selected
|
||||
// to add a nested entry but has not yet selected the nested field
|
||||
test('should set "disableAnd" to true if last exception item is a nested entry with no entries itself', () => {
|
||||
const exceptions: ExceptionsBuilderExceptionItem[] = [
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [getEntryListMock()],
|
||||
},
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [getEntryMatchMock(), { ...getEntryNestedMock(), entries: [] }],
|
||||
},
|
||||
];
|
||||
const { disableAnd } = reducer(initialState, {
|
||||
type: 'setExceptions',
|
||||
exceptions,
|
||||
});
|
||||
|
||||
expect(disableAnd).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should set "disableAnd" to false if last exception item is a nested entry with no entries itself', () => {
|
||||
const exceptions: ExceptionsBuilderExceptionItem[] = [
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [getEntryMatchMock(), getEntryNestedMock()],
|
||||
},
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [getEntryMatchMock()],
|
||||
},
|
||||
];
|
||||
const { disableAnd } = reducer(initialState, {
|
||||
type: 'setExceptions',
|
||||
exceptions,
|
||||
});
|
||||
|
||||
expect(disableAnd).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setDefault', () => {
|
||||
test('should restore initial state and add default empty entry to item" ', () => {
|
||||
const update = reducer(
|
||||
{
|
||||
disableAnd: false,
|
||||
disableNested: false,
|
||||
disableOr: false,
|
||||
andLogicIncluded: true,
|
||||
addNested: false,
|
||||
exceptions: [getExceptionListItemSchemaMock()],
|
||||
exceptionsToDelete: [],
|
||||
errorExists: 0,
|
||||
},
|
||||
{
|
||||
type: 'setDefault',
|
||||
initialState,
|
||||
lastException: {
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(update).toEqual({
|
||||
...initialState,
|
||||
exceptions: [
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [getDefaultEmptyEntry()],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setExceptionsToDelete', () => {
|
||||
test('should add passed in exception item to "exceptionsToDelete"', () => {
|
||||
const exceptions: ExceptionsBuilderExceptionItem[] = [
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
id: '1',
|
||||
entries: [getEntryListMock()],
|
||||
},
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
id: '2',
|
||||
entries: [getEntryMatchMock(), { ...getEntryNestedMock(), entries: [] }],
|
||||
},
|
||||
];
|
||||
const { exceptionsToDelete } = reducer(
|
||||
{
|
||||
disableAnd: false,
|
||||
disableNested: false,
|
||||
disableOr: false,
|
||||
andLogicIncluded: true,
|
||||
addNested: false,
|
||||
exceptions,
|
||||
exceptionsToDelete: [],
|
||||
errorExists: 0,
|
||||
},
|
||||
{
|
||||
type: 'setExceptionsToDelete',
|
||||
exceptions: [
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
id: '1',
|
||||
entries: [getEntryListMock()],
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
expect(exceptionsToDelete).toEqual([
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
id: '1',
|
||||
entries: [getEntryListMock()],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setDisableAnd', () => {
|
||||
test('should set "disableAnd" to false if "action.shouldDisable" is false', () => {
|
||||
const { disableAnd } = reducer(
|
||||
{
|
||||
disableAnd: true,
|
||||
disableNested: false,
|
||||
disableOr: false,
|
||||
andLogicIncluded: true,
|
||||
addNested: false,
|
||||
exceptions: [getExceptionListItemSchemaMock()],
|
||||
exceptionsToDelete: [],
|
||||
errorExists: 0,
|
||||
},
|
||||
{
|
||||
type: 'setDisableAnd',
|
||||
shouldDisable: false,
|
||||
}
|
||||
);
|
||||
|
||||
expect(disableAnd).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should set "disableAnd" to true if "action.shouldDisable" is true', () => {
|
||||
const { disableAnd } = reducer(
|
||||
{
|
||||
disableAnd: false,
|
||||
disableNested: false,
|
||||
disableOr: false,
|
||||
andLogicIncluded: true,
|
||||
addNested: false,
|
||||
exceptions: [getExceptionListItemSchemaMock()],
|
||||
exceptionsToDelete: [],
|
||||
errorExists: 0,
|
||||
},
|
||||
{
|
||||
type: 'setDisableAnd',
|
||||
shouldDisable: true,
|
||||
}
|
||||
);
|
||||
|
||||
expect(disableAnd).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setDisableOr', () => {
|
||||
test('should set "disableOr" to false if "action.shouldDisable" is false', () => {
|
||||
const { disableOr } = reducer(
|
||||
{
|
||||
disableAnd: false,
|
||||
disableNested: false,
|
||||
disableOr: true,
|
||||
andLogicIncluded: true,
|
||||
addNested: false,
|
||||
exceptions: [getExceptionListItemSchemaMock()],
|
||||
exceptionsToDelete: [],
|
||||
errorExists: 0,
|
||||
},
|
||||
{
|
||||
type: 'setDisableOr',
|
||||
shouldDisable: false,
|
||||
}
|
||||
);
|
||||
|
||||
expect(disableOr).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should set "disableOr" to true if "action.shouldDisable" is true', () => {
|
||||
const { disableOr } = reducer(
|
||||
{
|
||||
disableAnd: false,
|
||||
disableNested: false,
|
||||
disableOr: false,
|
||||
andLogicIncluded: true,
|
||||
addNested: false,
|
||||
exceptions: [getExceptionListItemSchemaMock()],
|
||||
exceptionsToDelete: [],
|
||||
errorExists: 0,
|
||||
},
|
||||
{
|
||||
type: 'setDisableOr',
|
||||
shouldDisable: true,
|
||||
}
|
||||
);
|
||||
|
||||
expect(disableOr).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setAddNested', () => {
|
||||
test('should set "addNested" to false if "action.addNested" is false', () => {
|
||||
const { addNested } = reducer(
|
||||
{
|
||||
disableAnd: false,
|
||||
disableNested: true,
|
||||
disableOr: false,
|
||||
andLogicIncluded: true,
|
||||
addNested: true,
|
||||
exceptions: [getExceptionListItemSchemaMock()],
|
||||
exceptionsToDelete: [],
|
||||
errorExists: 0,
|
||||
},
|
||||
{
|
||||
type: 'setAddNested',
|
||||
addNested: false,
|
||||
}
|
||||
);
|
||||
|
||||
expect(addNested).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should set "disableOr" to true if "action.addNested" is true', () => {
|
||||
const { addNested } = reducer(
|
||||
{
|
||||
disableAnd: false,
|
||||
disableNested: false,
|
||||
disableOr: false,
|
||||
andLogicIncluded: true,
|
||||
addNested: false,
|
||||
exceptions: [getExceptionListItemSchemaMock()],
|
||||
exceptionsToDelete: [],
|
||||
errorExists: 0,
|
||||
},
|
||||
{
|
||||
type: 'setAddNested',
|
||||
addNested: true,
|
||||
}
|
||||
);
|
||||
|
||||
expect(addNested).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setErrorsExist', () => {
|
||||
test('should increase "errorExists" by one if payload is "true"', () => {
|
||||
const { errorExists } = reducer(
|
||||
{
|
||||
disableAnd: false,
|
||||
disableNested: true,
|
||||
disableOr: false,
|
||||
andLogicIncluded: true,
|
||||
addNested: true,
|
||||
exceptions: [getExceptionListItemSchemaMock()],
|
||||
exceptionsToDelete: [],
|
||||
errorExists: 0,
|
||||
},
|
||||
{
|
||||
type: 'setErrorsExist',
|
||||
errorExists: true,
|
||||
}
|
||||
);
|
||||
|
||||
expect(errorExists).toEqual(1);
|
||||
});
|
||||
|
||||
test('should decrease "errorExists" by one if payload is "false"', () => {
|
||||
const { errorExists } = reducer(
|
||||
{
|
||||
disableAnd: false,
|
||||
disableNested: false,
|
||||
disableOr: false,
|
||||
andLogicIncluded: true,
|
||||
addNested: false,
|
||||
exceptions: [getExceptionListItemSchemaMock()],
|
||||
exceptionsToDelete: [],
|
||||
errorExists: 1,
|
||||
},
|
||||
{
|
||||
type: 'setErrorsExist',
|
||||
errorExists: false,
|
||||
}
|
||||
);
|
||||
|
||||
expect(errorExists).toEqual(0);
|
||||
});
|
||||
|
||||
test('should not decrease "errorExists" if decreasing would dip into negative numbers', () => {
|
||||
const { errorExists } = reducer(
|
||||
{
|
||||
disableAnd: false,
|
||||
disableNested: false,
|
||||
disableOr: false,
|
||||
andLogicIncluded: true,
|
||||
addNested: false,
|
||||
exceptions: [getExceptionListItemSchemaMock()],
|
||||
exceptionsToDelete: [],
|
||||
errorExists: 0,
|
||||
},
|
||||
{
|
||||
type: 'setErrorsExist',
|
||||
errorExists: false,
|
||||
}
|
||||
);
|
||||
|
||||
expect(errorExists).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,72 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const FIELD = i18n.translate('xpack.securitySolution.exceptions.builder.fieldDescription', {
|
||||
defaultMessage: 'Field',
|
||||
});
|
||||
|
||||
export const OPERATOR = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.builder.operatorDescription',
|
||||
{
|
||||
defaultMessage: 'Operator',
|
||||
}
|
||||
);
|
||||
|
||||
export const VALUE = i18n.translate('xpack.securitySolution.exceptions.builder.valueDescription', {
|
||||
defaultMessage: 'Value',
|
||||
});
|
||||
|
||||
export const EXCEPTION_FIELD_PLACEHOLDER = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.builder.exceptionFieldPlaceholderDescription',
|
||||
{
|
||||
defaultMessage: 'Search',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXCEPTION_FIELD_NESTED_PLACEHOLDER = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.builder.exceptionFieldNestedPlaceholderDescription',
|
||||
{
|
||||
defaultMessage: 'Search nested field',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXCEPTION_OPERATOR_PLACEHOLDER = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.builder.exceptionOperatorPlaceholderDescription',
|
||||
{
|
||||
defaultMessage: 'Operator',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXCEPTION_FIELD_VALUE_PLACEHOLDER = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.builder.exceptionFieldValuePlaceholderDescription',
|
||||
{
|
||||
defaultMessage: 'Search field value...',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXCEPTION_FIELD_LISTS_PLACEHOLDER = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.builder.exceptionListsPlaceholderDescription',
|
||||
{
|
||||
defaultMessage: 'Search for list...',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_NESTED_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.builder.addNestedDescription',
|
||||
{
|
||||
defaultMessage: 'Add nested condition',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_NON_NESTED_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.builder.addNonNestedDescription',
|
||||
{
|
||||
defaultMessage: 'Add non-nested condition',
|
||||
}
|
||||
);
|
|
@ -21,13 +21,13 @@ import { useAddOrUpdateException } from '../use_add_exception';
|
|||
import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index';
|
||||
import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
|
||||
import { EntriesArray } from '../../../../../../lists/common/schemas/types';
|
||||
import * as builder from '../builder';
|
||||
import {
|
||||
getRulesEqlSchemaMock,
|
||||
getRulesSchemaMock,
|
||||
} from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks';
|
||||
import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async';
|
||||
import { getMockTheme } from '../../../lib/kibana/kibana_react.mock';
|
||||
import { ExceptionBuilder } from '../../../../shared_imports';
|
||||
|
||||
const mockTheme = getMockTheme({
|
||||
eui: {
|
||||
|
@ -46,19 +46,28 @@ jest.mock('../use_add_exception');
|
|||
jest.mock('../../../containers/source');
|
||||
jest.mock('../use_fetch_or_create_rule_exception_list');
|
||||
jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_index');
|
||||
jest.mock('../builder');
|
||||
jest.mock('../../../../detections/containers/detection_engine/rules/use_rule_async');
|
||||
jest.mock('../../../../shared_imports', () => {
|
||||
const originalModule = jest.requireActual('../../../../shared_imports');
|
||||
|
||||
return {
|
||||
...originalModule,
|
||||
ExceptionBuilder: {
|
||||
ExceptionBuilderComponent: () => ({} as JSX.Element),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('When the edit exception modal is opened', () => {
|
||||
const ruleName = 'test rule';
|
||||
|
||||
let ExceptionBuilderComponent: jest.SpyInstance<
|
||||
ReturnType<typeof builder.ExceptionBuilderComponent>
|
||||
ReturnType<typeof ExceptionBuilder.ExceptionBuilderComponent>
|
||||
>;
|
||||
|
||||
beforeEach(() => {
|
||||
ExceptionBuilderComponent = jest
|
||||
.spyOn(builder, 'ExceptionBuilderComponent')
|
||||
.spyOn(ExceptionBuilder, 'ExceptionBuilderComponent')
|
||||
.mockReturnValue(<></>);
|
||||
|
||||
(useSignalIndex as jest.Mock).mockReturnValue({
|
||||
|
|
|
@ -22,7 +22,11 @@ import {
|
|||
EuiCallOut,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { hasEqlSequenceQuery, isEqlRule } from '../../../../../common/detection_engine/utils';
|
||||
import {
|
||||
hasEqlSequenceQuery,
|
||||
isEqlRule,
|
||||
isThresholdRule,
|
||||
} from '../../../../../common/detection_engine/utils';
|
||||
import { useFetchIndex } from '../../../containers/source';
|
||||
import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index';
|
||||
import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async';
|
||||
|
@ -30,12 +34,12 @@ import {
|
|||
ExceptionListItemSchema,
|
||||
CreateExceptionListItemSchema,
|
||||
ExceptionListType,
|
||||
} from '../../../../../public/lists_plugin_deps';
|
||||
ExceptionBuilder,
|
||||
} from '../../../../../public/shared_imports';
|
||||
import * as i18n from './translations';
|
||||
import * as sharedI18n from '../translations';
|
||||
import { useKibana } from '../../../lib/kibana';
|
||||
import { useAppToasts } from '../../../hooks/use_app_toasts';
|
||||
import { ExceptionBuilderComponent } from '../builder';
|
||||
import { useAddOrUpdateException } from '../use_add_exception';
|
||||
import { AddExceptionComments } from '../add_exception_comments';
|
||||
import {
|
||||
|
@ -44,6 +48,7 @@ import {
|
|||
entryHasListType,
|
||||
entryHasNonEcsType,
|
||||
lowercaseHashValues,
|
||||
filterIndexPatterns,
|
||||
} from '../helpers';
|
||||
import { Loader } from '../../loader';
|
||||
import { ErrorInfo, ErrorCallout } from '../error_callout';
|
||||
|
@ -312,13 +317,17 @@ export const EditExceptionModal = memo(function EditExceptionModal({
|
|||
)}
|
||||
<EuiText>{i18n.EXCEPTION_BUILDER_INFO}</EuiText>
|
||||
<EuiSpacer />
|
||||
<ExceptionBuilderComponent
|
||||
<ExceptionBuilder.ExceptionBuilderComponent
|
||||
allowLargeValueLists={
|
||||
!isEqlRule(maybeRule?.type) && !isThresholdRule(maybeRule?.type)
|
||||
}
|
||||
httpService={http}
|
||||
autocompleteService={data.autocomplete}
|
||||
exceptionListItems={[exceptionItem]}
|
||||
listType={exceptionListType}
|
||||
listId={exceptionItem.list_id}
|
||||
listNamespaceType={exceptionItem.namespace_type}
|
||||
listTypeSpecificIndexPatternFilter={filterIndexPatterns}
|
||||
ruleName={ruleName}
|
||||
isOrDisabled
|
||||
isAndDisabled={false}
|
||||
|
@ -327,7 +336,6 @@ export const EditExceptionModal = memo(function EditExceptionModal({
|
|||
id-aria="edit-exception-modal-builder"
|
||||
onChange={handleBuilderOnChange}
|
||||
indexPatterns={indexPatterns}
|
||||
ruleType={maybeRule?.type}
|
||||
/>
|
||||
|
||||
<EuiSpacer />
|
||||
|
|
|
@ -30,6 +30,7 @@ import {
|
|||
getFileCodeSignature,
|
||||
getProcessCodeSignature,
|
||||
retrieveAlertOsTypes,
|
||||
filterIndexPatterns,
|
||||
} from './helpers';
|
||||
import { AlertData, EmptyEntry } from './types';
|
||||
import {
|
||||
|
@ -49,6 +50,7 @@ import { getEntryMatchAnyMock } from '../../../../../lists/common/schemas/types/
|
|||
import { getEntryExistsMock } from '../../../../../lists/common/schemas/types/entry_exists.mock';
|
||||
import { getEntryListMock } from '../../../../../lists/common/schemas/types/entry_list.mock';
|
||||
import { getCommentsArrayMock } from '../../../../../lists/common/schemas/types/comment.mock';
|
||||
import { fields } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
|
||||
import {
|
||||
ENTRIES,
|
||||
ENTRIES_WITH_IDS,
|
||||
|
@ -60,12 +62,45 @@ import {
|
|||
EntriesArray,
|
||||
OsTypeArray,
|
||||
} from '../../../../../lists/common/schemas';
|
||||
import { IIndexPattern } from 'src/plugins/data/common';
|
||||
import { IFieldType, IIndexPattern } from 'src/plugins/data/common';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn().mockReturnValue('123'),
|
||||
}));
|
||||
|
||||
const getMockIndexPattern = (): IIndexPattern => ({
|
||||
fields,
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
});
|
||||
|
||||
const mockEndpointFields = [
|
||||
{
|
||||
name: 'file.path.caseless',
|
||||
type: 'string',
|
||||
esTypes: ['keyword'],
|
||||
count: 0,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: false,
|
||||
readFromDocValues: false,
|
||||
},
|
||||
{
|
||||
name: 'file.Ext.code_signature.status',
|
||||
type: 'string',
|
||||
esTypes: ['text'],
|
||||
count: 0,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: false,
|
||||
readFromDocValues: false,
|
||||
subType: { nested: { path: 'file.Ext.code_signature' } },
|
||||
},
|
||||
];
|
||||
|
||||
export const getEndpointField = (name: string) =>
|
||||
mockEndpointFields.find((field) => field.name === name) as IFieldType;
|
||||
|
||||
describe('Exception helpers', () => {
|
||||
beforeEach(() => {
|
||||
moment.tz.setDefault('UTC');
|
||||
|
@ -75,6 +110,25 @@ describe('Exception helpers', () => {
|
|||
moment.tz.setDefault('Browser');
|
||||
});
|
||||
|
||||
describe('#filterIndexPatterns', () => {
|
||||
test('it returns index patterns without filtering if list type is "detection"', () => {
|
||||
const mockIndexPatterns = getMockIndexPattern();
|
||||
const output = filterIndexPatterns(mockIndexPatterns, 'detection');
|
||||
|
||||
expect(output).toEqual(mockIndexPatterns);
|
||||
});
|
||||
|
||||
test('it returns filtered index patterns if list type is "endpoint"', () => {
|
||||
const mockIndexPatterns = {
|
||||
...getMockIndexPattern(),
|
||||
fields: [...fields, ...mockEndpointFields],
|
||||
};
|
||||
const output = filterIndexPatterns(mockIndexPatterns, 'endpoint');
|
||||
|
||||
expect(output).toEqual({ ...getMockIndexPattern(), fields: [...mockEndpointFields] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getOperatorType', () => {
|
||||
test('returns operator type "match" if entry.type is "match"', () => {
|
||||
const payload = getEntryMatchMock();
|
||||
|
|
|
@ -41,6 +41,7 @@ import {
|
|||
OsTypeArray,
|
||||
EntriesArray,
|
||||
osType,
|
||||
ExceptionListType,
|
||||
} from '../../../shared_imports';
|
||||
import { IIndexPattern } from '../../../../../../../src/plugins/data/common';
|
||||
import { validate } from '../../../../common/validate';
|
||||
|
@ -48,6 +49,19 @@ import { Ecs } from '../../../../common/ecs';
|
|||
import { CodeSignature } from '../../../../common/ecs/file';
|
||||
import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard';
|
||||
import { addIdToItem, removeIdFromItem } from '../../../../common';
|
||||
import exceptionableFields from './exceptionable_fields.json';
|
||||
|
||||
export const filterIndexPatterns = (
|
||||
patterns: IIndexPattern,
|
||||
type: ExceptionListType
|
||||
): IIndexPattern => {
|
||||
return type === 'endpoint'
|
||||
? {
|
||||
...patterns,
|
||||
fields: patterns.fields.filter(({ name }) => exceptionableFields.includes(name)),
|
||||
}
|
||||
: patterns;
|
||||
};
|
||||
|
||||
export const addIdToEntries = (entries: EntriesArray): EntriesArray => {
|
||||
return entries.map((singleEntry) => {
|
||||
|
|
|
@ -58,8 +58,5 @@ export {
|
|||
UseExceptionListItemsSuccess,
|
||||
addEndpointExceptionList,
|
||||
withOptionalSignal,
|
||||
BuilderEntryItem,
|
||||
BuilderAndBadgeComponent,
|
||||
BuilderEntryDeleteButtonComponent,
|
||||
BuilderExceptionListItemComponent,
|
||||
ExceptionBuilder,
|
||||
} from '../../lists/public';
|
||||
|
|
|
@ -19716,16 +19716,6 @@
|
|||
"xpack.securitySolution.exceptions.addException.sequenceWarning": "このルールのクエリにはEQLシーケンス文があります。作成された例外は、シーケンスのすべてのイベントに適用されます。",
|
||||
"xpack.securitySolution.exceptions.addException.success": "正常に例外を追加しました",
|
||||
"xpack.securitySolution.exceptions.andDescription": "AND",
|
||||
"xpack.securitySolution.exceptions.builder.addNestedDescription": "ネストされた条件を追加",
|
||||
"xpack.securitySolution.exceptions.builder.addNonNestedDescription": "ネストされていない条件を追加",
|
||||
"xpack.securitySolution.exceptions.builder.exceptionFieldNestedPlaceholderDescription": "ネストされたフィールドを検索",
|
||||
"xpack.securitySolution.exceptions.builder.exceptionFieldPlaceholderDescription": "検索",
|
||||
"xpack.securitySolution.exceptions.builder.exceptionFieldValuePlaceholderDescription": "検索フィールド値...",
|
||||
"xpack.securitySolution.exceptions.builder.exceptionListsPlaceholderDescription": "リストを検索...",
|
||||
"xpack.securitySolution.exceptions.builder.exceptionOperatorPlaceholderDescription": "演算子",
|
||||
"xpack.securitySolution.exceptions.builder.fieldDescription": "フィールド",
|
||||
"xpack.securitySolution.exceptions.builder.operatorDescription": "演算子",
|
||||
"xpack.securitySolution.exceptions.builder.valueDescription": "値",
|
||||
"xpack.securitySolution.exceptions.cancelLabel": "キャンセル",
|
||||
"xpack.securitySolution.exceptions.clearExceptionsLabel": "例外リストを削除",
|
||||
"xpack.securitySolution.exceptions.commentEventLabel": "コメントを追加しました",
|
||||
|
|
|
@ -20011,16 +20011,6 @@
|
|||
"xpack.securitySolution.exceptions.addException.sequenceWarning": "此规则的查询包含 EQL 序列语句。创建的例外将应用于序列中的所有事件。",
|
||||
"xpack.securitySolution.exceptions.addException.success": "已成功添加例外",
|
||||
"xpack.securitySolution.exceptions.andDescription": "AND",
|
||||
"xpack.securitySolution.exceptions.builder.addNestedDescription": "添加嵌套条件",
|
||||
"xpack.securitySolution.exceptions.builder.addNonNestedDescription": "添加非嵌套条件",
|
||||
"xpack.securitySolution.exceptions.builder.exceptionFieldNestedPlaceholderDescription": "搜索嵌套字段",
|
||||
"xpack.securitySolution.exceptions.builder.exceptionFieldPlaceholderDescription": "搜索",
|
||||
"xpack.securitySolution.exceptions.builder.exceptionFieldValuePlaceholderDescription": "搜索字段值......",
|
||||
"xpack.securitySolution.exceptions.builder.exceptionListsPlaceholderDescription": "搜索列表......",
|
||||
"xpack.securitySolution.exceptions.builder.exceptionOperatorPlaceholderDescription": "运算符",
|
||||
"xpack.securitySolution.exceptions.builder.fieldDescription": "字段",
|
||||
"xpack.securitySolution.exceptions.builder.operatorDescription": "运算符",
|
||||
"xpack.securitySolution.exceptions.builder.valueDescription": "值",
|
||||
"xpack.securitySolution.exceptions.cancelLabel": "取消",
|
||||
"xpack.securitySolution.exceptions.clearExceptionsLabel": "移除例外列表",
|
||||
"xpack.securitySolution.exceptions.commentEventLabel": "已添加注释",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue