Fix close all alerts in exceptions linked to shared exception with multiple rules (#189604)

## Fix close all alerts in exceptions linked to shared exception with
multiple rules

close: https://github.com/elastic/kibana/issues/189282

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Khristinin Nikita 2024-09-09 11:04:59 +02:00 committed by GitHub
parent e308d17195
commit 7b84fa8394
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 209 additions and 6 deletions

View file

@ -10,7 +10,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import {
buildAlertStatusesFilter,
buildAlertsFilter,
buildAlertsFilterByRuleIds,
} from '../../../detections/components/alerts_table/default_config';
import { getEsQueryFilter } from '../../../detections/containers/detection_engine/exceptions/get_es_query_filter';
import type { IndexPatternArray } from '../../../../common/api/detection_engine/model/rule_schema';
@ -76,10 +76,12 @@ export const useCloseAlertsFromExceptions = (): ReturnUseCloseAlertsFromExceptio
'in-progress',
]);
const filterByRuleIds = buildAlertsFilterByRuleIds(ruleStaticIds);
const filter = await getEsQueryFilter(
'',
'kuery',
[...ruleStaticIds.flatMap((id) => buildAlertsFilter(id)), ...alertStatusFilter],
[...filterByRuleIds, ...alertStatusFilter],
bulkCloseIndex,
prepareExceptionItemsForBulkClose(exceptionItems),
false

View file

@ -16,6 +16,7 @@ import {
buildThreatMatchFilter,
getAlertsDefaultModel,
getAlertsPreviewDefaultModel,
buildAlertsFilterByRuleIds,
} from './default_config';
jest.mock('./actions');
@ -283,6 +284,75 @@ describe('alerts default_config', () => {
});
});
describe('buildAlertsFilterByRuleIds', () => {
it('given an empty list of rule ids will return an empty filter', () => {
const filters = buildAlertsFilterByRuleIds([]);
expect(filters).toHaveLength(0);
});
it('builds filter containing 1 rule id passed into function', () => {
const filters = buildAlertsFilterByRuleIds(['rule-id-1']);
const expected = {
meta: {
alias: null,
disabled: false,
negate: false,
},
query: {
bool: {
should: [
{
term: {
'kibana.alert.rule.rule_id': 'rule-id-1',
},
},
],
minimum_should_match: 1,
},
},
};
expect(filters).toHaveLength(1);
expect(filters[0]).toEqual(expected);
});
it('builds filter containing 3 rule ids passed into function', () => {
const filters = buildAlertsFilterByRuleIds(['rule-id-1', 'rule-id-2', 'rule-id-3']);
const expected = {
meta: {
alias: null,
disabled: false,
negate: false,
},
query: {
bool: {
should: [
{
term: {
'kibana.alert.rule.rule_id': 'rule-id-1',
},
},
{
term: {
'kibana.alert.rule.rule_id': 'rule-id-2',
},
},
{
term: {
'kibana.alert.rule.rule_id': 'rule-id-3',
},
},
],
minimum_should_match: 1,
},
},
};
expect(filters).toHaveLength(1);
expect(filters[0]).toEqual(expected);
});
});
describe('getAlertsDefaultModel', () => {
test('returns correct model for Basic license', () => {
const licenseServiceMock = createLicenseServiceMock();

View file

@ -122,6 +122,34 @@ export const buildAlertsFilter = (ruleStaticId: string | null): Filter[] =>
]
: [];
export const buildAlertsFilterByRuleIds = (ruleIds: string[] | null): Filter[] => {
if (ruleIds == null || ruleIds.length === 0) {
return [];
}
const combinedQuery = {
bool: {
should: ruleIds.map((ruleId) => ({
term: {
[ALERT_RULE_RULE_ID]: ruleId,
},
})),
minimum_should_match: 1,
},
};
return [
{
meta: {
alias: null,
negate: false,
disabled: false,
},
query: combinedQuery,
},
];
};
export const buildShowBuildingBlockFilter = (showBuildingBlockAlerts: boolean): Filter[] =>
showBuildingBlockAlerts
? []

View file

@ -4,6 +4,12 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
createExceptionList,
createExceptionListItem,
deleteExceptionLists,
linkRulesToExceptionList,
} from '../../../../../../tasks/api_calls/exceptions';
import { waitForAlertsToPopulate } from '../../../../../../tasks/create_new_rule';
import {
addExceptionFromFirstAlert,
@ -12,25 +18,42 @@ import {
} from '../../../../../../tasks/alerts';
import { deleteAlertsAndRules, postDataView } from '../../../../../../tasks/api_calls/common';
import { login } from '../../../../../../tasks/login';
import { clickDisableRuleSwitch, visitRuleDetailsPage } from '../../../../../../tasks/rule_details';
import {
clickDisableRuleSwitch,
visitRuleDetailsPage,
openEditException,
goToExceptionsTab,
goToAlertsTab,
} from '../../../../../../tasks/rule_details';
import { createRule } from '../../../../../../tasks/api_calls/rules';
import { getNewRule } from '../../../../../../objects/rule';
import { getExceptionList } from '../../../../../../objects/exception';
import { LOADING_INDICATOR } from '../../../../../../screens/security_header';
import { ALERTS_COUNT } from '../../../../../../screens/alerts';
import { ALERTS_COUNT, ALERT_EMBEDDABLE_EMPTY_PROMPT } from '../../../../../../screens/alerts';
import {
addExceptionEntryFieldValue,
addExceptionEntryOperatorValue,
addExceptionEntryFieldValueValue,
addExceptionFlyoutItemName,
selectBulkCloseAlerts,
submitEditedExceptionItem,
submitNewExceptionItem,
} from '../../../../../../tasks/exceptions';
const EXCEPTION_LIST_NAME = 'My test list';
const getExceptionList1 = () => ({
...getExceptionList(),
name: EXCEPTION_LIST_NAME,
list_id: 'exception_list_1',
});
describe('Close matching Alerts ', { tags: ['@ess', '@serverless'] }, () => {
const ITEM_NAME = 'Sample Exception Item';
beforeEach(() => {
deleteAlertsAndRules();
deleteExceptionLists();
cy.task('esArchiverUnload', { archiveName: 'exceptions' });
cy.task('esArchiverLoad', { archiveName: 'exceptions' });
@ -54,6 +77,7 @@ describe('Close matching Alerts ', { tags: ['@ess', '@serverless'] }, () => {
after(() => {
cy.task('esArchiverUnload', { archiveName: 'exceptions' });
deleteAlertsAndRules();
deleteExceptionLists();
});
it('Should create a Rule exception item from alert actions overflow menu and close all matching alerts', () => {
@ -77,4 +101,55 @@ describe('Close matching Alerts ', { tags: ['@ess', '@serverless'] }, () => {
cy.get(LOADING_INDICATOR).should('not.exist');
cy.get(ALERTS_COUNT).should('contain', '1');
});
it('Should close all alerts from if several rules has shared exception list', () => {
cy.get(ALERTS_COUNT).should('contain', '3');
let exceptionListId = '';
createExceptionList(getExceptionList1(), getExceptionList1().list_id)
.then((response) => {
exceptionListId = response.body.id;
createExceptionListItem(getExceptionList1().list_id, {
item_id: '123',
entries: [
{
field: 'user.name',
operator: 'included',
type: 'match_any',
value: ['alice'],
},
],
});
})
.then((response) => {
return createRule(
getNewRule({
exceptions_list: [
{
id: response.body.id,
list_id: getExceptionList1().list_id,
type: getExceptionList1().type,
namespace_type: getExceptionList1().namespace_type,
},
],
})
);
})
.then(() =>
linkRulesToExceptionList('rule_testing', {
id: exceptionListId,
listId: getExceptionList1().list_id,
})
);
goToExceptionsTab();
cy.reload();
openEditException();
selectBulkCloseAlerts();
submitEditedExceptionItem();
goToAlertsTab();
cy.get(ALERTS_COUNT).should('not.exist');
cy.get(ALERT_EMBEDDABLE_EMPTY_PROMPT).should('exist');
});
});

View file

@ -31,7 +31,7 @@ export interface ExceptionListItem {
namespace_type: 'single' | 'agnostic';
tags: string[];
type: 'simple';
entries: Array<{ field: string; operator: string; type: string; value: string[] }>;
entries: Array<{ field: string; operator: string; type: string; value: string[] | string }>;
expire_time?: string;
}

View file

@ -47,7 +47,7 @@ export const createExceptionList = (
export const createExceptionListItem = (
exceptionListId: string,
exceptionListItem?: ExceptionListItem
exceptionListItem?: Partial<ExceptionListItem>
) =>
rootRequest<ExceptionListItemSchema>({
method: 'POST',
@ -128,3 +128,27 @@ export const deleteExceptionLists = () => {
export const deleteEndpointExceptionList = () => {
deleteExceptionList('endpoint_list', 'agnostic');
};
export const linkRulesToExceptionList = (
ruleId: string,
exceptionList: {
id: string;
listId: string;
}
) => {
rootRequest({
method: 'PATCH',
url: `/api/detection_engine/rules`,
body: {
exceptions_list: [
{
id: exceptionList.id,
list_id: exceptionList.listId,
namespace_type: 'single',
type: 'detection',
},
],
rule_id: ruleId,
},
});
};

View file

@ -36,6 +36,10 @@
"user": {
"type": "nested",
"properties": {
"name": {
"ignore_above": 1024,
"type": "keyword"
},
"first": {
"type": "keyword"
},