mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Security Solution][Alerts] Detection engine wildcard exceptions (#136147)
* Implement wildcard exceptions for detection rules * Fix index pattern retrieval on edit exceptions flyout * Fix API integration test logic * Fix entry_renderer linting * Remove bad fix idea * Add 'does not match' operator to UI * Fix test * Add unit tests * Add wildcard exceptions to list of DE exception operators Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
17b7cd75c7
commit
aaa3107dbc
10 changed files with 512 additions and 10 deletions
|
@ -132,6 +132,9 @@ describe('operator', () => {
|
|||
{
|
||||
label: 'matches',
|
||||
},
|
||||
{
|
||||
label: 'does not match',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
@ -115,6 +124,8 @@ export const DETECTION_ENGINE_EXCEPTION_OPERATORS: OperatorOption[] = [
|
|||
doesNotExistOperator,
|
||||
isInListOperator,
|
||||
isNotInListOperator,
|
||||
matchesOperator,
|
||||
doesNotMatchOperator,
|
||||
];
|
||||
|
||||
export const ALL_OPERATORS: OperatorOption[] = [
|
||||
|
@ -127,6 +138,7 @@ export const ALL_OPERATORS: OperatorOption[] = [
|
|||
isInListOperator,
|
||||
isNotInListOperator,
|
||||
matchesOperator,
|
||||
doesNotMatchOperator,
|
||||
];
|
||||
|
||||
export const EXCEPTION_OPERATORS_SANS_LISTS: OperatorOption[] = [
|
||||
|
@ -136,6 +148,8 @@ export const EXCEPTION_OPERATORS_SANS_LISTS: OperatorOption[] = [
|
|||
isNotOneOfOperator,
|
||||
existsOperator,
|
||||
doesNotExistOperator,
|
||||
matchesOperator,
|
||||
doesNotMatchOperator,
|
||||
];
|
||||
|
||||
export const EXCEPTION_OPERATORS_ONLY_LISTS: OperatorOption[] = [
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
@ -290,6 +292,25 @@ export const buildMatchAnyClause = (entry: EntryMatchAny): BooleanFilter => {
|
|||
}
|
||||
};
|
||||
|
||||
export const buildMatchWildcardClause = (entry: EntryMatchWildcard): BooleanFilter => {
|
||||
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 = {
|
||||
|
@ -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 {
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
buildExistsClause,
|
||||
buildMatchAnyClause,
|
||||
buildMatchClause,
|
||||
buildMatchWildcardClause,
|
||||
buildNestedClause,
|
||||
createOrClauses,
|
||||
} from '@kbn/securitysolution-list-utils';
|
||||
|
@ -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
|
||||
|
||||
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,6 +10,7 @@ import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
|
|||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import {
|
||||
doesNotExistOperator,
|
||||
doesNotMatchOperator,
|
||||
existsOperator,
|
||||
isInListOperator,
|
||||
isNotInListOperator,
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
|
|
@ -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:
|
||||
|
@ -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;
|
||||
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
|
||||
|
|
|
@ -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)`
|
||||
|
|
|
@ -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',
|
||||
{
|
||||
|
|
|
@ -593,5 +593,173 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
expect(hits).to.eql(['word four', 'word one', 'word three', 'word two']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('"matches wildcard" operator', () => {
|
||||
it('should return 0 alerts if wildcard matches all words', async () => {
|
||||
const rule = getRuleForSignalTesting(['keyword']);
|
||||
const { id } = await createRuleWithExceptionEntries(supertest, log, rule, [
|
||||
[
|
||||
{
|
||||
field: 'keyword',
|
||||
operator: 'included',
|
||||
type: 'wildcard',
|
||||
// Filter out all 4 words
|
||||
value: 'word *',
|
||||
},
|
||||
],
|
||||
]);
|
||||
await waitForRuleSuccessOrStatus(supertest, log, id);
|
||||
const signalsOpen = await getSignalsById(supertest, log, id);
|
||||
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort();
|
||||
expect(hits).to.eql([]);
|
||||
});
|
||||
|
||||
it('should return 1 alert if wildcard exceptions match one, two, and three', async () => {
|
||||
const rule = getRuleForSignalTesting(['keyword']);
|
||||
const { id } = await createRuleWithExceptionEntries(supertest, log, rule, [
|
||||
[
|
||||
{
|
||||
field: 'keyword',
|
||||
operator: 'included',
|
||||
type: 'wildcard',
|
||||
value: 'word one',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
field: 'keyword',
|
||||
operator: 'included',
|
||||
type: 'wildcard',
|
||||
// Filter out both "word two" and "word three"
|
||||
value: 'word t*',
|
||||
},
|
||||
],
|
||||
]);
|
||||
await waitForRuleSuccessOrStatus(supertest, log, id);
|
||||
await waitForSignalsToBePresent(supertest, log, 1, [id]);
|
||||
const signalsOpen = await getSignalsById(supertest, log, id);
|
||||
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort();
|
||||
expect(hits).to.eql(['word four']);
|
||||
});
|
||||
|
||||
it('should return 3 alerts if one is set as an exception', async () => {
|
||||
const rule = getRuleForSignalTesting(['keyword']);
|
||||
const { id } = await createRuleWithExceptionEntries(supertest, log, rule, [
|
||||
[
|
||||
{
|
||||
field: 'keyword',
|
||||
operator: 'included',
|
||||
type: 'wildcard',
|
||||
// Without * or ? in the value, it should work the same as the "is" operator
|
||||
value: 'word one',
|
||||
},
|
||||
],
|
||||
]);
|
||||
await waitForRuleSuccessOrStatus(supertest, log, id);
|
||||
await waitForSignalsToBePresent(supertest, log, 3, [id]);
|
||||
const signalsOpen = await getSignalsById(supertest, log, id);
|
||||
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort();
|
||||
expect(hits).to.eql(['word four', 'word three', 'word two']);
|
||||
});
|
||||
|
||||
it('should return 4 alerts if the wildcard matches nothing', async () => {
|
||||
const rule = getRuleForSignalTesting(['keyword']);
|
||||
const { id } = await createRuleWithExceptionEntries(supertest, log, rule, [
|
||||
[
|
||||
{
|
||||
field: 'keyword',
|
||||
operator: 'included',
|
||||
type: 'wildcard',
|
||||
value: 'word a*',
|
||||
},
|
||||
],
|
||||
]);
|
||||
await waitForRuleSuccessOrStatus(supertest, log, id);
|
||||
await waitForSignalsToBePresent(supertest, log, 4, [id]);
|
||||
const signalsOpen = await getSignalsById(supertest, log, id);
|
||||
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort();
|
||||
expect(hits).to.eql(['word four', 'word one', 'word three', 'word two']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('"does not match wildcard" operator', () => {
|
||||
it('should return 4 results if excluded wildcard matches all 4 words', async () => {
|
||||
const rule = getRuleForSignalTesting(['keyword']);
|
||||
const { id } = await createRuleWithExceptionEntries(supertest, log, rule, [
|
||||
[
|
||||
{
|
||||
field: 'keyword',
|
||||
operator: 'excluded',
|
||||
type: 'wildcard',
|
||||
// Filter out everything except things matching 'word *'
|
||||
value: 'word *',
|
||||
},
|
||||
],
|
||||
]);
|
||||
await waitForRuleSuccessOrStatus(supertest, log, id);
|
||||
await waitForSignalsToBePresent(supertest, log, 4, [id]);
|
||||
const signalsOpen = await getSignalsById(supertest, log, id);
|
||||
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort();
|
||||
expect(hits).to.eql(['word four', 'word one', 'word three', 'word two']);
|
||||
});
|
||||
|
||||
it('should filter in 2 keywords if using a wildcard exception', async () => {
|
||||
const rule = getRuleForSignalTesting(['keyword']);
|
||||
const { id } = await createRuleWithExceptionEntries(supertest, log, rule, [
|
||||
[
|
||||
{
|
||||
field: 'keyword',
|
||||
operator: 'excluded',
|
||||
type: 'wildcard',
|
||||
// Filter out everything except things matching 'word t*'
|
||||
value: 'word t*',
|
||||
},
|
||||
],
|
||||
]);
|
||||
await waitForRuleSuccessOrStatus(supertest, log, id);
|
||||
await waitForSignalsToBePresent(supertest, log, 2, [id]);
|
||||
const signalsOpen = await getSignalsById(supertest, log, id);
|
||||
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort();
|
||||
expect(hits).to.eql(['word three', 'word two']);
|
||||
});
|
||||
|
||||
it('should return 1 alert if "word one" is excluded', async () => {
|
||||
const rule = getRuleForSignalTesting(['keyword']);
|
||||
const { id } = await createRuleWithExceptionEntries(supertest, log, rule, [
|
||||
[
|
||||
{
|
||||
field: 'keyword',
|
||||
operator: 'excluded',
|
||||
type: 'wildcard',
|
||||
// Without * or ? in the value, it should work the same as the "is not" operator
|
||||
value: 'word one',
|
||||
},
|
||||
],
|
||||
]);
|
||||
await waitForRuleSuccessOrStatus(supertest, log, id);
|
||||
await waitForSignalsToBePresent(supertest, log, 1, [id]);
|
||||
const signalsOpen = await getSignalsById(supertest, log, id);
|
||||
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort();
|
||||
expect(hits).to.eql(['word one']);
|
||||
});
|
||||
|
||||
it('should return 0 alerts if it cannot find what it is excluding', async () => {
|
||||
const rule = getRuleForSignalTesting(['keyword']);
|
||||
const { id } = await createRuleWithExceptionEntries(supertest, log, rule, [
|
||||
[
|
||||
{
|
||||
field: 'keyword',
|
||||
operator: 'excluded',
|
||||
type: 'wildcard',
|
||||
value: 'word a*',
|
||||
},
|
||||
],
|
||||
]);
|
||||
await waitForRuleSuccessOrStatus(supertest, log, id);
|
||||
const signalsOpen = await getSignalsById(supertest, log, id);
|
||||
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort();
|
||||
expect(hits).to.eql([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -633,5 +633,134 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('"matches wildcard" operator', () => {
|
||||
it('should filter 1 single keyword if it is set as an exception', async () => {
|
||||
const rule = getRuleForSignalTesting(['keyword_as_array']);
|
||||
const { id } = await createRuleWithExceptionEntries(supertest, log, rule, [
|
||||
[
|
||||
{
|
||||
field: 'keyword',
|
||||
operator: 'included',
|
||||
type: 'wildcard',
|
||||
value: 'word o*',
|
||||
},
|
||||
],
|
||||
]);
|
||||
await waitForRuleSuccessOrStatus(supertest, log, id);
|
||||
await waitForSignalsToBePresent(supertest, log, 3, [id]);
|
||||
const signalsOpen = await getSignalsById(supertest, log, id);
|
||||
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort();
|
||||
expect(hits).to.eql([
|
||||
[],
|
||||
['word eight', 'word nine', 'word ten'],
|
||||
['word five', null, 'word six', 'word seven'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should filter 2 keyword if both are set as exceptions', async () => {
|
||||
const rule = getRuleForSignalTesting(['keyword_as_array']);
|
||||
const { id } = await createRuleWithExceptionEntries(supertest, log, rule, [
|
||||
[
|
||||
{
|
||||
field: 'keyword',
|
||||
operator: 'included',
|
||||
type: 'wildcard',
|
||||
value: 'word t*',
|
||||
},
|
||||
],
|
||||
]);
|
||||
await waitForRuleSuccessOrStatus(supertest, log, id);
|
||||
await waitForSignalsToBePresent(supertest, log, 2, [id]);
|
||||
const signalsOpen = await getSignalsById(supertest, log, id);
|
||||
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort();
|
||||
expect(hits).to.eql([[], ['word five', null, 'word six', 'word seven']]);
|
||||
});
|
||||
|
||||
it('should filter 3 keyword if all 3 are set as exceptions', async () => {
|
||||
const rule = getRuleForSignalTesting(['keyword_as_array']);
|
||||
const { id } = await createRuleWithExceptionEntries(supertest, log, rule, [
|
||||
[
|
||||
{
|
||||
field: 'keyword',
|
||||
operator: 'included',
|
||||
type: 'wildcard',
|
||||
value: 'word *',
|
||||
},
|
||||
],
|
||||
]);
|
||||
await waitForRuleSuccessOrStatus(supertest, log, id);
|
||||
await waitForSignalsToBePresent(supertest, log, 1, [id]);
|
||||
const signalsOpen = await getSignalsById(supertest, log, id);
|
||||
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort();
|
||||
expect(hits.flat(Number.MAX_SAFE_INTEGER)).to.eql([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('"does not match wildcard" operator', () => {
|
||||
it('should filter 1 single keyword if it is set as an exception', async () => {
|
||||
const rule = getRuleForSignalTesting(['keyword_as_array']);
|
||||
const { id } = await createRuleWithExceptionEntries(supertest, log, rule, [
|
||||
[
|
||||
{
|
||||
field: 'keyword',
|
||||
operator: 'excluded',
|
||||
type: 'wildcard',
|
||||
value: 'word o*',
|
||||
},
|
||||
],
|
||||
]);
|
||||
await waitForRuleSuccessOrStatus(supertest, log, id);
|
||||
await waitForSignalsToBePresent(supertest, log, 1, [id]);
|
||||
const signalsOpen = await getSignalsById(supertest, log, id);
|
||||
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort();
|
||||
expect(hits).to.eql([['word one', 'word two', 'word three', 'word four']]);
|
||||
});
|
||||
|
||||
it('should filter 2 keyword if both are set as exceptions', async () => {
|
||||
const rule = getRuleForSignalTesting(['keyword_as_array']);
|
||||
const { id } = await createRuleWithExceptionEntries(supertest, log, rule, [
|
||||
[
|
||||
{
|
||||
field: 'keyword',
|
||||
operator: 'excluded',
|
||||
type: 'wildcard',
|
||||
value: 'word t*',
|
||||
},
|
||||
],
|
||||
]);
|
||||
await waitForRuleSuccessOrStatus(supertest, log, id);
|
||||
await waitForSignalsToBePresent(supertest, log, 2, [id]);
|
||||
const signalsOpen = await getSignalsById(supertest, log, id);
|
||||
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort();
|
||||
expect(hits).to.eql([
|
||||
['word eight', 'word nine', 'word ten'],
|
||||
['word one', 'word two', 'word three', 'word four'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should filter 3 keyword if all 3 are set as exceptions', async () => {
|
||||
const rule = getRuleForSignalTesting(['keyword_as_array']);
|
||||
const { id } = await createRuleWithExceptionEntries(supertest, log, rule, [
|
||||
[
|
||||
{
|
||||
field: 'keyword',
|
||||
operator: 'excluded',
|
||||
type: 'wildcard',
|
||||
value: 'word *',
|
||||
},
|
||||
],
|
||||
]);
|
||||
await waitForRuleSuccessOrStatus(supertest, log, id);
|
||||
await waitForSignalsToBePresent(supertest, log, 3, [id]);
|
||||
const signalsOpen = await getSignalsById(supertest, log, id);
|
||||
const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort();
|
||||
expect(hits).to.eql([
|
||||
['word eight', 'word nine', 'word ten'],
|
||||
['word five', null, 'word six', 'word seven'],
|
||||
['word one', 'word two', 'word three', 'word four'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue