[DE][Exceptions] Allow numerous match_any values that differ in case (#167208)

## Summary

Updates the exceptions flyout UI `match_any` operator to accept numerous
duplicate values that differ in case. Prior to this change, a user could
not add a field value of `foo` and `FOO` - the UI would display that the
value is a duplicate. We now will allow this as exceptions are case
sensitive and this is a necessary use case for the current exceptions
behavior.

Cypress tests and FTR tests are added.
This commit is contained in:
Yara Tercero 2023-09-28 17:52:39 -07:00 committed by GitHub
parent 8a29a5e2ca
commit e9d2e782b5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 248 additions and 12 deletions

View file

@ -184,6 +184,7 @@ export const AutocompleteFieldMatchAnyComponent: React.FC<AutocompleteFieldMatch
onSearchChange={handleSearchChange}
onCreateOption={handleCreateOption}
isInvalid={selectedField != null && error != null}
isCaseSensitive
onBlur={setIsTouchedValue}
data-test-subj="valuesAutocompleteMatchAny"
fullWidth

View file

@ -620,6 +620,61 @@ export default ({ getService }: FtrProviderContext) => {
expect(signalsOpen.hits.hits.length).toEqual(0);
});
it('should be able to execute against an exception list that does include valid case sensitive entries and get back 0 signals', async () => {
const rule: QueryRuleCreateProps = {
name: 'Simple Rule Query',
description: 'Simple Rule Query',
enabled: true,
risk_score: 1,
rule_id: 'rule-1',
severity: 'high',
index: ['auditbeat-*'],
type: 'query',
from: '1900-01-01T00:00:00.000Z',
query: 'host.name: "suricata-sensor-amsterdam"',
};
const rule2: QueryRuleCreateProps = {
name: 'Simple Rule Query',
description: 'Simple Rule Query',
enabled: true,
risk_score: 1,
rule_id: 'rule-2',
severity: 'high',
index: ['auditbeat-*'],
type: 'query',
from: '1900-01-01T00:00:00.000Z',
query: 'host.name: "suricata-sensor-amsterdam"',
};
const createdRule = await createRuleWithExceptionEntries(supertest, log, rule, [
[
{
field: 'host.os.name',
operator: 'included',
type: 'match_any',
value: ['ubuntu'],
},
],
]);
const createdRule2 = await createRuleWithExceptionEntries(supertest, log, rule2, [
[
{
field: 'host.os.name', // This matches the query above which will exclude everything
operator: 'included',
type: 'match_any',
value: ['ubuntu', 'Ubuntu'],
},
],
]);
const signalsOpen = await getOpenSignals(supertest, log, es, createdRule);
const signalsOpen2 = await getOpenSignals(supertest, log, es, createdRule2);
// Expect signals here because all values are "Ubuntu"
// and exception is one of ["ubuntu"]
expect(signalsOpen.hits.hits.length).toEqual(10);
// Expect no signals here because all values are "Ubuntu"
// and exception is one of ["ubuntu", "Ubuntu"]
expect(signalsOpen2.hits.hits.length).toEqual(0);
});
it('generates no signals when an exception is added for an EQL rule', async () => {
const rule: EqlRuleCreateProps = {
...getEqlRuleForSignalTesting(['auditbeat-*']),

View file

@ -67,6 +67,37 @@ export default ({ getService }: FtrProviderContext) => {
expect(bodyToCompare).to.eql(getExceptionListItemResponseMockWithoutAutoGeneratedValues());
});
it('should create a match any exception item with multiple case sensitive values', async () => {
const entries = [
{
field: 'agent.name',
operator: 'included',
type: 'match_any',
value: ['dll', 'DLL'],
},
];
await supertest
.post(EXCEPTION_LIST_URL)
.set('kbn-xsrf', 'true')
.send(getCreateExceptionListMinimalSchemaMock())
.expect(200);
const { body } = await supertest
.post(EXCEPTION_LIST_ITEM_URL)
.set('kbn-xsrf', 'true')
.send({
...getCreateExceptionListItemMinimalSchemaMock(),
entries,
})
.expect(200);
const bodyToCompare = removeExceptionListItemServerGeneratedProperties(body);
expect(bodyToCompare).to.eql({
...getExceptionListItemResponseMockWithoutAutoGeneratedValues(),
entries,
});
});
it('should create a simple exception list item without an id', async () => {
await supertest
.post(EXCEPTION_LIST_URL)

View file

@ -125,7 +125,7 @@ describe.skip('Exceptions flyout', { tags: ['@ess', '@serverless', '@skipInServe
cy.get(CONFIRM_BTN).should('be.disabled');
// add value again and button should be enabled again
addExceptionEntryFieldMatchAnyValue('test', 0);
addExceptionEntryFieldMatchAnyValue(['test'], 0);
cy.get(CONFIRM_BTN).should('be.enabled');
});

View file

@ -0,0 +1,89 @@
/*
* 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 { getNewRule } from '../../../objects/rule';
import { RULE_STATUS } from '../../../screens/create_new_rule';
import { createRule } from '../../../tasks/api_calls/rules';
import { login } from '../../../tasks/login';
import {
openExceptionFlyoutFromEmptyViewerPrompt,
visitRuleDetailsPage,
enablesRule,
waitForTheRuleToBeExecuted,
goToAlertsTab,
} from '../../../tasks/rule_details';
import {
addExceptionEntryFieldMatchAnyValue,
addExceptionEntryFieldValue,
addExceptionEntryOperatorValue,
addExceptionFlyoutItemName,
submitNewExceptionItem,
} from '../../../tasks/exceptions';
import { CONFIRM_BTN } from '../../../screens/exceptions';
import { deleteAlertsAndRules } from '../../../tasks/common';
import { ALERTS_COUNT } from '../../../screens/alerts';
import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule';
describe('Exceptions match_any', { tags: ['@ess', '@serverless'] }, () => {
before(() => {
// this is a made-up index that has just the necessary
// mappings to conduct tests, avoiding loading large
// amounts of data like in auditbeat_exceptions
cy.task('esArchiverLoad', { archiveName: 'exceptions' });
});
beforeEach(() => {
deleteAlertsAndRules();
login();
createRule(
getNewRule({
index: ['exceptions-*'],
enabled: false,
query: '*',
from: 'now-438300h',
})
).then((rule) => visitRuleDetailsPage(rule.body.id, { tab: 'rule_exceptions' }));
cy.get(RULE_STATUS).should('have.text', '—');
});
after(() => {
cy.task('esArchiverUnload', 'exceptions');
});
it('Creates exception item', () => {
cy.log('open add exception modal');
openExceptionFlyoutFromEmptyViewerPrompt();
cy.log('add exception item name');
addExceptionFlyoutItemName('My item name');
cy.log('add match_any entry');
addExceptionEntryFieldValue('agent.name', 0);
// Asserting double negative because it is easier to check
// that an alert was created than that it was NOT (as if it is not
// it could be for other reasons, like rule failure)
addExceptionEntryOperatorValue('is not one of', 0);
addExceptionEntryFieldMatchAnyValue(['foo', 'FOO'], 0);
cy.get(CONFIRM_BTN).should('be.enabled');
submitNewExceptionItem();
enablesRule();
goToAlertsTab();
waitForTheRuleToBeExecuted();
waitForAlertsToPopulate();
// Will match document with value "foo" and document with value "FOO"
cy.log('Asserting that alert is generated');
cy.get(ALERTS_COUNT)
.invoke('text')
.should('match', /^[2].+$/);
});
});

View file

@ -63,7 +63,7 @@ describe(
'Add/edit exception from rule details',
{ tags: ['@ess', '@serverless', '@brokenInServerless'] },
() => {
const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert';
const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '3 alerts';
const FIELD_DIFFERENT_FROM_EXISTING_ITEM_FIELD = 'agent.name';
const ITEM_FIELD = 'unique_value.test';
@ -276,8 +276,8 @@ describe(
// add exception item conditions
addExceptionConditions({
field: 'agent.name',
operator: 'is',
values: ['foo'],
operator: 'is one of',
values: ['foo', 'FOO', 'bar'],
});
// Name is required so want to check that submit is still disabled

View file

@ -45,7 +45,7 @@ describe(
'Add exception using data views from rule details',
{ tags: ['@ess', '@serverless', '@brokenInServerless'] },
() => {
const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert';
const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '3 alerts';
const ITEM_NAME = 'Sample Exception List Item';
before(() => {
@ -87,8 +87,8 @@ describe(
addFirstExceptionFromRuleDetails(
{
field: 'agent.name',
operator: 'is',
values: ['foo'],
operator: 'is one of',
values: ['foo', 'FOO', 'bar'],
},
ITEM_NAME
);

View file

@ -114,8 +114,10 @@ export const addExceptionEntryFieldValueValue = (value: string, index = 0) => {
cy.get(EXCEPTION_FLYOUT_TITLE).click();
};
export const addExceptionEntryFieldMatchAnyValue = (value: string, index = 0) => {
cy.get(VALUES_MATCH_ANY_INPUT).eq(index).type(`${value}{enter}`);
export const addExceptionEntryFieldMatchAnyValue = (values: string[], index = 0) => {
values.forEach((value) => {
cy.get(VALUES_MATCH_ANY_INPUT).eq(index).type(`${value}{enter}`);
});
cy.get(EXCEPTION_FLYOUT_TITLE).click();
};
export const addExceptionEntryFieldMatchIncludedValue = (value: string, index = 0) => {
@ -164,9 +166,13 @@ export const selectCloseSingleAlerts = () => {
export const addExceptionConditions = (exception: Exception) => {
cy.get(FIELD_INPUT).type(`${exception.field}{downArrow}{enter}`);
cy.get(OPERATOR_INPUT).type(`${exception.operator}{enter}`);
exception.values.forEach((value) => {
cy.get(VALUES_INPUT).type(`${value}{enter}`);
});
if (exception.operator === 'is one of') {
addExceptionEntryFieldMatchAnyValue(exception.values, 0);
} else {
exception.values.forEach((value) => {
cy.get(VALUES_INPUT).type(`${value}{enter}`);
});
}
};
export const validateExceptionConditionField = (value: string) => {

View file

@ -24,3 +24,57 @@
}
}
}
{
"type": "doc",
"value": {
"id": "_aZE5nwBOpWiDweSth_A",
"index": "exceptions-0001",
"source": {
"@timestamp": "2019-09-01T00:41:04.527Z",
"agent": {
"name": "bar"
},
"unique_value": {
"test": "another value"
},
"user" : [
{
"name" : "john",
"id" : "c5baec68-e774-46dc-b728-417e71d68444"
},
{
"name" : "alice",
"id" : "6e831997-deab-4e56-9218-a90ef045556e"
}
]
}
}
}
{
"type": "doc",
"value": {
"id": "_aZE5nwBOpWiDweSth_C",
"index": "exceptions-0001",
"source": {
"@timestamp": "2019-09-01T00:41:09.527Z",
"agent": {
"name": "FOO"
},
"unique_value": {
"test": "different value"
},
"user" : [
{
"name" : "john",
"id" : "c5baec68-e774-46dc-b728-417e71d68444"
},
{
"name" : "alice",
"id" : "6e831997-deab-4e56-9218-a90ef045556e"
}
]
}
}
}