Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ describe('operator', () => {
{
label: 'matches',
},
{
label: 'does not match',
},
]);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,15 @@ export const matchesOperator: OperatorOption = {
value: 'matches',
};

export const doesNotMatchOperator: OperatorOption = {
message: i18n.translate('lists.exceptions.doesNotMatchOperatorLabel', {
defaultMessage: 'does not match',
}),
operator: OperatorEnum.EXCLUDED,
type: OperatorTypeEnum.WILDCARD,
value: 'does_not_match',
};

export const EVENT_FILTERS_OPERATORS: OperatorOption[] = [
isOperator,
isNotOperator,
Expand All @@ -112,6 +121,7 @@ export const EXCEPTION_OPERATORS: OperatorOption[] = [
isInListOperator,
isNotInListOperator,
matchesOperator,
doesNotMatchOperator,
];

export const EXCEPTION_OPERATORS_SANS_LISTS: OperatorOption[] = [
Expand All @@ -121,6 +131,8 @@ export const EXCEPTION_OPERATORS_SANS_LISTS: OperatorOption[] = [
isNotOneOfOperator,
existsOperator,
doesNotExistOperator,
matchesOperator,
doesNotMatchOperator,
];

export const EXCEPTION_OPERATORS_ONLY_LISTS: OperatorOption[] = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ import {
entriesMatchAny,
entriesNested,
OsTypeArray,
entriesMatchWildcard,
EntryMatchWildcard,
} from '@kbn/securitysolution-io-ts-list-types';
import { Filter } from '@kbn/es-query';

import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { hasLargeValueList } from '../has_large_value_list';

type NonListEntry = EntryMatch | EntryMatchAny | EntryNested | EntryExists;
type NonListEntry = EntryMatch | EntryMatchAny | EntryNested | EntryExists | EntryMatchWildcard;
interface ExceptionListItemNonLargeList extends ExceptionListItemSchema {
entries: NonListEntry[];
}
Expand Down Expand Up @@ -290,6 +292,25 @@ export const buildMatchAnyClause = (entry: EntryMatchAny): BooleanFilter => {
}
};

export const buildMatchWildcardClause = (entry: EntryMatchWildcard): BooleanFilter => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a unit test for this like the others here - x-pack/plugins/lists/common/exceptions/build_exceptions_filter.test.ts ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!

const { field, operator, value } = entry;
const wildcardClause = {
bool: {
filter: {
wildcard: {
[field]: value,
},
},
},
};

if (operator === 'excluded') {
return buildExclusionClause(wildcardClause);
} else {
return wildcardClause;
}
};

export const buildExistsClause = (entry: EntryExists): BooleanFilter => {
const { field, operator } = entry;
const existsClause = {
Expand Down Expand Up @@ -352,15 +373,15 @@ export const createInnerAndClauses = (
entry: NonListEntry,
parent?: string
): BooleanFilter | NestedFilter => {
const field = parent != null ? `${parent}.${entry.field}` : entry.field;
if (entriesExists.is(entry)) {
const field = parent != null ? `${parent}.${entry.field}` : entry.field;
return buildExistsClause({ ...entry, field });
} else if (entriesMatch.is(entry)) {
const field = parent != null ? `${parent}.${entry.field}` : entry.field;
return buildMatchClause({ ...entry, field });
} else if (entriesMatchAny.is(entry)) {
const field = parent != null ? `${parent}.${entry.field}` : entry.field;
return buildMatchAnyClause({ ...entry, field });
} else if (entriesMatchWildcard.is(entry)) {
return buildMatchWildcardClause({ ...entry, field });
} else if (entriesNested.is(entry)) {
return buildNestedClause(entry);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
buildExistsClause,
buildMatchAnyClause,
buildMatchClause,
buildMatchWildcardClause,
buildNestedClause,
createOrClauses,
} from '@kbn/securitysolution-list-utils';
Expand All @@ -32,6 +33,10 @@ import {
getEntryNestedMock,
} from '../schemas/types/entry_nested.mock';
import { getExceptionListItemSchemaMock } from '../schemas/response/exception_list_item_schema.mock';
import {
getEntryMatchWildcardExcludeMock,
getEntryMatchWildcardMock,
} from '../schemas/types/entry_match_wildcard.mock';

// TODO: Port the test over to packages/kbn-securitysolution-list-utils/src/build_exception_filter/index.test.ts once the mocks are ported to kbn

Expand Down Expand Up @@ -1040,4 +1045,38 @@ describe('build_exceptions_filter', () => {
});
});
});

describe('buildWildcardClause', () => {
test('it should build wildcard filter when operator is "included"', () => {
const booleanFilter = buildMatchWildcardClause(getEntryMatchWildcardMock());

expect(booleanFilter).toEqual({
bool: {
filter: {
wildcard: {
'host.name': 'some host name',
},
},
},
});
});

test('it should build boolean filter when operator is "excluded"', () => {
const booleanFilter = buildMatchWildcardClause(getEntryMatchWildcardExcludeMock());

expect(booleanFilter).toEqual({
bool: {
must_not: {
bool: {
filter: {
wildcard: {
'host.name': 'some host name',
},
},
},
},
},
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { coreMock } from '@kbn/core/public/mocks';
import {
doesNotExistOperator,
doesNotMatchOperator,
existsOperator,
isInListOperator,
isNotInListOperator,
Expand Down Expand Up @@ -383,6 +384,80 @@ describe('BuilderEntryItem', () => {
).toBeTruthy();
});

test('it renders field values correctly when operator is "matchesOperator"', () => {
wrapper = mount(
<BuilderEntryItem
autocompleteService={autocompleteStartMock}
entry={{
correspondingKeywordField: undefined,
entryIndex: 0,
field: getField('ip'),
id: '123',
nested: undefined,
operator: matchesOperator,
parent: undefined,
value: '1234*',
}}
httpService={mockKibanaHttpService}
indexPattern={{
fields,
id: '1234',
title: 'logstash-*',
}}
listType="detection"
onChange={jest.fn()}
setErrorsExist={jest.fn()}
setWarningsExist={jest.fn()}
showLabel={false}
/>
);

expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip');
expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual(
'matches'
);
expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldWildcard"]').text()).toEqual(
'1234*'
);
});

test('it renders field values correctly when operator is "doesNotMatchOperator"', () => {
wrapper = mount(
<BuilderEntryItem
autocompleteService={autocompleteStartMock}
entry={{
correspondingKeywordField: undefined,
entryIndex: 0,
field: getField('ip'),
id: '123',
nested: undefined,
operator: doesNotMatchOperator,
parent: undefined,
value: '1234*',
}}
httpService={mockKibanaHttpService}
indexPattern={{
fields,
id: '1234',
title: 'logstash-*',
}}
listType="detection"
onChange={jest.fn()}
setErrorsExist={jest.fn()}
setWarningsExist={jest.fn()}
showLabel={false}
/>
);

expect(wrapper.find('[data-test-subj="exceptionBuilderEntryField"]').text()).toEqual('ip');
expect(wrapper.find('[data-test-subj="exceptionBuilderEntryOperator"]').text()).toEqual(
'does not match'
);
expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldWildcard"]').text()).toEqual(
'1234*'
);
});

test('it uses "correspondingKeywordField" if it exists', () => {
const correspondingKeywordField: FieldSpec = {
aggregatable: true,
Expand Down Expand Up @@ -656,6 +731,47 @@ describe('BuilderEntryItem', () => {
);
});

test('it invokes "onChange" when new value field is entered for wildcard operator', () => {
const mockOnChange = jest.fn();
wrapper = mount(
<BuilderEntryItem
autocompleteService={autocompleteStartMock}
entry={{
correspondingKeywordField: undefined,
entryIndex: 0,
field: getField('ip'),
id: '123',
nested: undefined,
operator: matchesOperator,
parent: undefined,
value: '1234*',
}}
httpService={mockKibanaHttpService}
indexPattern={{
fields,
id: '1234',
title: 'logstash-*',
}}
listType="detection"
onChange={mockOnChange}
setErrorsExist={jest.fn()}
setWarningsExist={jest.fn()}
showLabel={false}
/>
);

(
wrapper.find(EuiComboBox).at(2).props() as unknown as {
onCreateOption: (a: string) => void;
}
).onCreateOption('5678*');

expect(mockOnChange).toHaveBeenCalledWith(
{ field: 'ip', id: '123', operator: 'included', type: 'wildcard', value: '5678*' },
0
);
});

test('it invokes "setErrorsExist" when user touches value input and leaves empty', async () => {
const mockSetErrorExists = jest.fn();
wrapper = mount(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
);
};

// eslint-disable-next-line complexity
const getFieldValueComboBox = (type: OperatorTypeEnum, isFirst: boolean): JSX.Element => {
switch (type) {
case OperatorTypeEnum.MATCH:
Expand Down Expand Up @@ -338,13 +339,16 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
);
case OperatorTypeEnum.WILDCARD:
const wildcardValue = typeof entry.value === 'string' ? entry.value : undefined;
let os: OperatingSystem = OperatingSystem.WINDOWS;
if (osTypes) {
[os] = osTypes as OperatingSystem[];
let actualWarning: React.ReactNode | string | undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know you didn't introduce wildcard here, but can we add a unit test for it in the existing entry_renderer tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!

if (listType !== 'detection') {
let os: OperatingSystem = OperatingSystem.WINDOWS;
if (osTypes) {
[os] = osTypes as OperatingSystem[];
}
const warning = validateFilePathInput({ os, value: wildcardValue });
actualWarning =
warning === FILENAME_WILDCARD_WARNING ? getWildcardWarning(warning) : warning;
}
const warning = validateFilePathInput({ os, value: wildcardValue });
const actualWarning =
warning === FILENAME_WILDCARD_WARNING ? getWildcardWarning(warning) : warning;

return (
<AutocompleteFieldWildcardComponent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const OPERATOR_TYPE_LABELS_INCLUDED = Object.freeze({
const OPERATOR_TYPE_LABELS_EXCLUDED = Object.freeze({
[ListOperatorTypeEnum.MATCH_ANY]: i18n.CONDITION_OPERATOR_TYPE_NOT_MATCH_ANY,
[ListOperatorTypeEnum.MATCH]: i18n.CONDITION_OPERATOR_TYPE_NOT_MATCH,
[ListOperatorTypeEnum.WILDCARD]: i18n.CONDITION_OPERATOR_TYPE_WILDCARD_DOES_NOT_MATCH,
});

const EuiFlexGroupNested = styled(EuiFlexGroup)`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ export const CONDITION_OPERATOR_TYPE_WILDCARD_MATCHES = i18n.translate(
}
);

export const CONDITION_OPERATOR_TYPE_WILDCARD_DOES_NOT_MATCH = i18n.translate(
'xpack.securitySolution.exceptions.exceptionItem.conditions.wildcardDoesNotMatchOperator',
{
defaultMessage: 'DOES NOT MATCH',
}
);

export const CONDITION_OPERATOR_TYPE_NESTED = i18n.translate(
'xpack.securitySolution.exceptions.exceptionItem.conditions.nestedOperator',
{
Expand Down
Loading