Skip to content

Commit

Permalink
[Security Solution][Exceptions] - Make esTypes and subType available …
Browse files Browse the repository at this point in the history
…to index patterns (elastic#72336)

## Summary

This PR updates the following:

- `useFetchIndexPatterns` now returns `indexPatterns` whose fields include `esTypes` and `subType`
  - Why?? The exceptions builder needs these two fields to determine what fields are of ES type `nested` and parent paths
- exceptions add and edit modals now use the `rule.index` field to pass into `useFetchindexPatterns`
  - Before we were using the signals index and alerts index for endpoint, needs to be rule's index patterns
  - if no index patterns exist on the rule (if rule created via API, it's not required), then uses `DEFAULT_INDEX_PATTERN`
- updates the autocomplete validation to use `IField.esTypes` to check type instead of `IField.type`
  • Loading branch information
yctercero committed Jul 20, 2020
1 parent e0dec6e commit 1f6837f
Show file tree
Hide file tree
Showing 24 changed files with 362 additions and 169 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,27 @@ import { ThemeProvider } from 'styled-components';
import { mount } from 'enzyme';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
// we don't have the types for waitFor just yet, so using "as waitFor" until when we do
import { wait as waitFor } from '@testing-library/react';

import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks.ts';
import { AutocompleteFieldListsComponent } from './field_value_lists';
import { ListSchema } from '../../../lists_plugin_deps';
import { getFoundListSchemaMock } from '../../../../../lists/common/schemas/response/found_list_schema.mock';
import { getListResponseMock } from '../../../../../lists/common/schemas/response/list_schema.mock';
import { DATE_NOW } from '../../../../../lists/common/constants.mock';

import { AutocompleteFieldListsComponent } from './field_value_lists';

const mockStart = jest.fn();
const mockResult = getFoundListSchemaMock();
jest.mock('../../../common/lib/kibana');
const mockStart = jest.fn();
const mockKeywordList: ListSchema = {
...getListResponseMock(),
id: 'keyword_list',
type: 'keyword',
name: 'keyword list',
};
const mockResult = { ...getFoundListSchemaMock() };
mockResult.data = [...mockResult.data, mockKeywordList];
jest.mock('../../../lists_plugin_deps', () => {
const originalModule = jest.requireActual('../../../lists_plugin_deps');

Expand All @@ -31,7 +44,7 @@ jest.mock('../../../lists_plugin_deps', () => {
});

describe('AutocompleteFieldListsComponent', () => {
test('it renders disabled if "isDisabled" is true', () => {
test('it renders disabled if "isDisabled" is true', async () => {
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<AutocompleteFieldListsComponent
Expand All @@ -46,14 +59,16 @@ describe('AutocompleteFieldListsComponent', () => {
</ThemeProvider>
);

expect(
wrapper
.find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] input`)
.prop('disabled')
).toBeTruthy();
await waitFor(() => {
expect(
wrapper
.find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] input`)
.prop('disabled')
).toBeTruthy();
});
});

test('it renders loading if "isLoading" is true', () => {
test('it renders loading if "isLoading" is true', async () => {
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<AutocompleteFieldListsComponent
Expand All @@ -67,20 +82,23 @@ describe('AutocompleteFieldListsComponent', () => {
/>
</ThemeProvider>
);
wrapper
.find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] button`)
.at(0)
.simulate('click');
expect(

await waitFor(() => {
wrapper
.find(
`EuiComboBoxOptionsList[data-test-subj="valuesAutocompleteComboBox listsComboxBox-optionsList"]`
)
.prop('isLoading')
).toBeTruthy();
.find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] button`)
.at(0)
.simulate('click');
expect(
wrapper
.find(
`EuiComboBoxOptionsList[data-test-subj="valuesAutocompleteComboBox listsComboxBox-optionsList"]`
)
.prop('isLoading')
).toBeTruthy();
});
});

test('it allows user to clear values if "isClearable" is true', () => {
test('it allows user to clear values if "isClearable" is true', async () => {
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<AutocompleteFieldListsComponent
Expand All @@ -94,15 +112,62 @@ describe('AutocompleteFieldListsComponent', () => {
/>
</ThemeProvider>
);

expect(
wrapper
.find(`[data-test-subj="comboBoxInput"]`)
.hasClass('euiComboBox__inputWrap-isClearable')
).toBeTruthy();
});

test('it correctly displays selected list', () => {
test('it correctly displays lists that match the selected "keyword" field esType', () => {
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<AutocompleteFieldListsComponent
placeholder="Placeholder text"
selectedField={getField('@tags')}
selectedValue=""
isLoading={false}
isClearable={false}
isDisabled={false}
onChange={jest.fn()}
/>
</ThemeProvider>
);

wrapper.find('[data-test-subj="comboBoxToggleListButton"] button').simulate('click');

expect(
wrapper
.find('EuiComboBox[data-test-subj="valuesAutocompleteComboBox listsComboxBox"]')
.prop('options')
).toEqual([{ label: 'keyword list' }]);
});

test('it correctly displays lists that match the selected "ip" field esType', () => {
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<AutocompleteFieldListsComponent
placeholder="Placeholder text"
selectedField={getField('ip')}
selectedValue=""
isLoading={false}
isClearable={false}
isDisabled={false}
onChange={jest.fn()}
/>
</ThemeProvider>
);

wrapper.find('[data-test-subj="comboBoxToggleListButton"] button').simulate('click');

expect(
wrapper
.find('EuiComboBox[data-test-subj="valuesAutocompleteComboBox listsComboxBox"]')
.prop('options')
).toEqual([{ label: 'some name' }]);
});

test('it correctly displays selected list', async () => {
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<AutocompleteFieldListsComponent
Expand Down Expand Up @@ -146,15 +211,15 @@ describe('AutocompleteFieldListsComponent', () => {
}).onChange([{ label: 'some name' }]);

expect(mockOnChange).toHaveBeenCalledWith({
created_at: '2020-04-20T15:25:31.830Z',
created_at: DATE_NOW,
created_by: 'some user',
description: 'some description',
id: 'some-list-id',
meta: {},
name: 'some name',
tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e',
type: 'ip',
updated_at: '2020-04-20T15:25:31.830Z',
updated_at: DATE_NOW,
updated_by: 'some user',
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,12 @@ export const AutocompleteFieldListsComponent: React.FC<AutocompleteFieldListsPro
const getLabel = useCallback(({ name }) => name, []);

const optionsMemo = useMemo(() => {
if (selectedField != null) {
return lists.filter(({ type }) => type === selectedField.type);
if (
selectedField != null &&
selectedField.esTypes != null &&
selectedField.esTypes.length > 0
) {
return lists.filter(({ type }) => selectedField.esTypes?.includes(type));
} else {
return [];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
});
};

const isValid = useMemo(
(): boolean => validateParams(selectedValue, selectedField ? selectedField.type : ''),
[selectedField, selectedValue]
);
const isValid = useMemo((): boolean => validateParams(selectedValue, selectedField), [
selectedField,
selectedValue,
]);

return (
<EuiComboBox
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export const AutocompleteFieldMatchAnyComponent: React.FC<AutocompleteFieldMatch

const isValid = useMemo((): boolean => {
const areAnyInvalid = selectedComboOptions.filter(
({ label }) => !validateParams(label, selectedField ? selectedField.type : '')
({ label }) => !validateParams(label, selectedField)
);
return areAnyInvalid.length === 0;
}, [selectedComboOptions, selectedField]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,49 +55,25 @@ describe('helpers', () => {

describe('#validateParams', () => {
test('returns true if value is undefined', () => {
const isValid = validateParams(undefined, 'date');
const isValid = validateParams(undefined, getField('@timestamp'));

expect(isValid).toBeTruthy();
});

test('returns true if value is empty string', () => {
const isValid = validateParams('', 'date');
const isValid = validateParams('', getField('@timestamp'));

expect(isValid).toBeTruthy();
});

test('returns true if type is "date" and value is valid', () => {
const isValid = validateParams('1994-11-05T08:15:30-05:00', 'date');
const isValid = validateParams('1994-11-05T08:15:30-05:00', getField('@timestamp'));

expect(isValid).toBeTruthy();
});

test('returns false if type is "date" and value is not valid', () => {
const isValid = validateParams('1593478826', 'date');

expect(isValid).toBeFalsy();
});

test('returns true if type is "ip" and value is valid', () => {
const isValid = validateParams('126.45.211.34', 'ip');

expect(isValid).toBeTruthy();
});

test('returns false if type is "ip" and value is not valid', () => {
const isValid = validateParams('hellooo', 'ip');

expect(isValid).toBeFalsy();
});

test('returns true if type is "number" and value is valid', () => {
const isValid = validateParams('123', 'number');

expect(isValid).toBeTruthy();
});

test('returns false if type is "number" and value is not valid', () => {
const isValid = validateParams('not a number', 'number');
const isValid = validateParams('1593478826', getField('@timestamp'));

expect(isValid).toBeFalsy();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import dateMath from '@elastic/datemath';
import { EuiComboBoxOptionOption } from '@elastic/eui';

import { IFieldType, Ipv4Address } from '../../../../../../../src/plugins/data/common';
import { IFieldType } from '../../../../../../../src/plugins/data/common';

import {
EXCEPTION_OPERATORS,
Expand All @@ -30,29 +30,27 @@ export const getOperators = (field: IFieldType | undefined): OperatorOption[] =>
}
};

export function validateParams(params: string | undefined, type: string) {
export const validateParams = (
params: string | undefined,
field: IFieldType | undefined
): boolean => {
// Box would show error state if empty otherwise
if (params == null || params === '') {
return true;
}

switch (type) {
case 'date':
const moment = dateMath.parse(params);
return Boolean(moment && moment.isValid());
case 'ip':
try {
return Boolean(new Ipv4Address(params));
} catch (e) {
return false;
}
case 'number':
const val = parseFloat(params);
return typeof val === 'number' && !isNaN(val);
default:
return true;
}
}
const types = field != null && field.esTypes != null ? field.esTypes : [];

return types.reduce<boolean>((acc, type) => {
switch (type) {
case 'date':
const moment = dateMath.parse(params);
return Boolean(moment && moment.isValid());
default:
return acc;
}
}, true);
};

export function getGenericComboBoxProps<T>({
options,
Expand Down
Loading

0 comments on commit 1f6837f

Please sign in to comment.