[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:
Yara Tercero 2021-04-06 11:26:15 -07:00 committed by GitHub
parent 7f97f8bc59
commit 92b9482875
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 824 additions and 1029 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 && (

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -58,8 +58,5 @@ export {
UseExceptionListItemsSuccess,
addEndpointExceptionList,
withOptionalSignal,
BuilderEntryItem,
BuilderAndBadgeComponent,
BuilderEntryDeleteButtonComponent,
BuilderExceptionListItemComponent,
ExceptionBuilder,
} from '../../lists/public';

View file

@ -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": "コメントを追加しました",

View file

@ -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": "已添加注释",