[Security solution][Detections] Fixes multiple Bulk Edit issues/suggestions and increases test coverage (#124525)

Issue: https://github.com/elastic/kibana/issues/125223
## Summary

- removes try/catch for bulk edit operation
- removes isElasticRule in bulk route API, replaces with rule.params.immutable check(as isElasticRule is used within telemetry)
- adds Cypress tests
- fixes case when index pattern action applied to ML rule. As ML rules don't have index pattern, so we will throw error if there is an attempt to run this action. It partially addresses https://github.com/elastic/kibana/issues/124918
- now checks if updated index patterns array is empty, if it is: throws error(https://github.com/elastic/kibana/issues/125223)
This commit is contained in:
Vitalii Dmyterko 2022-02-11 19:43:31 +00:00 committed by GitHub
parent 5629decd9e
commit ae51f810f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 736 additions and 192 deletions

View file

@ -0,0 +1,220 @@
/*
* 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 {
ELASTIC_RULES_BTN,
CUSTOM_RULES_BTN,
MODAL_CONFIRMATION_BTN,
SELECT_ALL_RULES_ON_PAGE_CHECKBOX,
LOAD_PREBUILT_RULES_ON_PAGE_HEADER_BTN,
RULES_TAGS_FILTER_BTN,
} from '../../screens/alerts_detection_rules';
import {
RULES_BULK_EDIT_OVERWRITE_INDEX_PATTERNS_CHECKBOX,
RULES_BULK_EDIT_OVERWRITE_TAGS_CHECKBOX,
RULES_BULK_EDIT_INDEX_PATTERNS_WARNING,
RULES_BULK_EDIT_TAGS_WARNING,
} from '../../screens/rules_bulk_edit';
import {
changeRowsPerPageTo,
waitForRulesTableToBeLoaded,
selectAllRules,
goToTheRuleDetailsOf,
waitForRulesTableToBeRefreshed,
selectNumberOfRules,
testAllTagsBadges,
} from '../../tasks/alerts_detection_rules';
import {
openBulkEditAddIndexPatternsForm,
openBulkEditDeleteIndexPatternsForm,
typeIndexPatterns,
waitForBulkEditActionToFinish,
confirmBulkEditForm,
clickAddIndexPatternsMenuItem,
waitForElasticRulesBulkEditModal,
waitForMixedRulesBulkEditModal,
openBulkEditAddTagsForm,
openBulkEditDeleteTagsForm,
typeTags,
} from '../../tasks/rules_bulk_edit';
import { hasIndexPatterns } from '../../tasks/rule_details';
import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { SECURITY_DETECTIONS_RULES_URL } from '../../urls/navigation';
import { createCustomRule } from '../../tasks/api_calls/rules';
import { cleanKibana } from '../../tasks/common';
import {
getExistingRule,
getNewOverrideRule,
getNewRule,
getNewThresholdRule,
totalNumberOfPrebuiltRules,
} from '../../objects/rule';
const RULE_NAME = 'Custom rule for bulk actions';
const CUSTOM_INDEX_PATTERN_1 = 'custom-cypress-test-*';
const DEFAULT_INDEX_PATTERNS = ['index-1-*', 'index-2-*'];
const TAGS = ['cypress-tag-1', 'cypress-tag-2'];
const OVERWRITE_INDEX_PATTERNS = ['overwrite-index-1-*', 'overwrite-index-2-*'];
const customRule = {
...getNewRule(),
index: DEFAULT_INDEX_PATTERNS,
name: RULE_NAME,
};
describe('Detection rules, bulk edit', () => {
beforeEach(() => {
cleanKibana();
createCustomRule(customRule, '1');
createCustomRule(getExistingRule(), '2');
createCustomRule(getNewOverrideRule(), '3');
createCustomRule(getNewThresholdRule(), '4');
createCustomRule({ ...getNewRule(), name: 'rule # 5' }, '5');
createCustomRule({ ...getNewRule(), name: 'rule # 6' }, '6');
loginAndWaitForPageWithoutDateRange(SECURITY_DETECTIONS_RULES_URL);
waitForRulesTableToBeLoaded();
});
it('should show modal windows when Elastic rules selected and edit only custom rules', () => {
cy.get(LOAD_PREBUILT_RULES_ON_PAGE_HEADER_BTN)
.pipe(($el) => $el.trigger('click'))
.should('not.exist');
// select few Elastic rules, check if we can't proceed further, as ELastic rules are not editable
// filter rules, only Elastic rule to show
cy.get(ELASTIC_RULES_BTN).click();
waitForRulesTableToBeRefreshed();
// check modal window for few selected rules
selectNumberOfRules(5);
clickAddIndexPatternsMenuItem();
waitForElasticRulesBulkEditModal(5);
cy.get(MODAL_CONFIRMATION_BTN).click();
// Select Elastic rules and custom rules, check mixed rules warning modal window, proceed with editing custom rules
cy.get(ELASTIC_RULES_BTN).click();
selectAllRules();
clickAddIndexPatternsMenuItem();
waitForMixedRulesBulkEditModal(totalNumberOfPrebuiltRules, 6);
cy.get(MODAL_CONFIRMATION_BTN).should('have.text', 'Edit custom rules').click();
typeIndexPatterns([CUSTOM_INDEX_PATTERN_1]);
confirmBulkEditForm();
// check if rule has been updated
cy.get(CUSTOM_RULES_BTN).click();
goToTheRuleDetailsOf(RULE_NAME);
hasIndexPatterns([...DEFAULT_INDEX_PATTERNS, CUSTOM_INDEX_PATTERN_1].join(''));
});
it('should add/delete/overwrite index patterns in rules', () => {
cy.log('Adds index patterns');
// Switch to 5 rules per page, so we can edit all existing rules, not only ones on a page
// this way we will use underlying bulk edit API with query parameter, which update all rules based on query search results
changeRowsPerPageTo(5);
selectAllRules();
openBulkEditAddIndexPatternsForm();
typeIndexPatterns([CUSTOM_INDEX_PATTERN_1]);
confirmBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: 6 });
// check if rule has been updated
changeRowsPerPageTo(20);
goToTheRuleDetailsOf(RULE_NAME);
hasIndexPatterns([...DEFAULT_INDEX_PATTERNS, CUSTOM_INDEX_PATTERN_1].join(''));
cy.go('back');
cy.log('Deletes index patterns');
// select all rules on page (as page displays all existing rules).
// this way we will use underlying bulk edit API with ids parameter, which updates rules based their ids
cy.get(SELECT_ALL_RULES_ON_PAGE_CHECKBOX).click();
openBulkEditDeleteIndexPatternsForm();
typeIndexPatterns([CUSTOM_INDEX_PATTERN_1]);
confirmBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: 6 });
// check if rule has been updated
goToTheRuleDetailsOf(RULE_NAME);
hasIndexPatterns(DEFAULT_INDEX_PATTERNS.join(''));
cy.go('back');
cy.log('Overwrites index patterns');
cy.get(SELECT_ALL_RULES_ON_PAGE_CHECKBOX).click();
openBulkEditAddIndexPatternsForm();
cy.get(RULES_BULK_EDIT_OVERWRITE_INDEX_PATTERNS_CHECKBOX)
.should('have.text', 'Overwrite all selected rules index patterns')
.click();
cy.get(RULES_BULK_EDIT_INDEX_PATTERNS_WARNING).should(
'have.text',
'Youre about to overwrite index patterns for 6 selected rules, press Save to apply changes.'
);
typeIndexPatterns(OVERWRITE_INDEX_PATTERNS);
confirmBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: 6 });
// check if rule has been updated
goToTheRuleDetailsOf(RULE_NAME);
hasIndexPatterns(OVERWRITE_INDEX_PATTERNS.join(''));
});
it('should add/delete/overwrite tags in rules', () => {
cy.log('Add tags to all rules');
// Switch to 5 rules per page, so we can edit all existing rules, not only ones on a page
// this way we will use underlying bulk edit API with query parameter, which update all rules based on query search results
changeRowsPerPageTo(5);
selectAllRules();
// open add tags form and add 2 new tags
openBulkEditAddTagsForm();
typeTags(TAGS);
confirmBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: 6 });
// check if all rules have been updated with new tags
changeRowsPerPageTo(20);
testAllTagsBadges(TAGS);
// test how many tags exist and displayed in filter button
cy.get(RULES_TAGS_FILTER_BTN).contains(/Tags2/);
cy.log('Remove one tag from all rules');
// select all rules on page (as page displays all existing rules).
// this way we will use underlying bulk edit API with query parameter, which update all rules based on query search results
cy.get(SELECT_ALL_RULES_ON_PAGE_CHECKBOX).click();
openBulkEditDeleteTagsForm();
typeTags([TAGS[0]]);
confirmBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: 6 });
testAllTagsBadges(TAGS.slice(1));
cy.get(RULES_TAGS_FILTER_BTN).contains(/Tags1/);
cy.log('Overwrite all tags');
openBulkEditAddTagsForm();
cy.get(RULES_BULK_EDIT_OVERWRITE_TAGS_CHECKBOX)
.should('have.text', 'Overwrite all selected rules tags')
.click();
cy.get(RULES_BULK_EDIT_TAGS_WARNING).should(
'have.text',
'Youre about to overwrite tags for 6 selected rules, press Save to apply changes.'
);
typeTags(['overwrite-tag']);
confirmBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: 6 });
testAllTagsBadges(['overwrite-tag']);
});
});

View file

@ -56,7 +56,6 @@ import {
CUSTOM_QUERY_DETAILS,
DEFINITION_DETAILS,
FALSE_POSITIVES_DETAILS,
getDetails,
removeExternalLinkText,
INDEX_PATTERNS_DETAILS,
INVESTIGATION_NOTES_MARKDOWN,
@ -102,7 +101,7 @@ import {
} from '../../tasks/create_new_rule';
import { saveEditedRule, waitForKibana } from '../../tasks/edit_rule';
import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { activatesRule } from '../../tasks/rule_details';
import { activatesRule, getDetails } from '../../tasks/rule_details';
import { RULE_CREATION, DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation';

View file

@ -26,7 +26,6 @@ import {
CUSTOM_QUERY_DETAILS,
DEFINITION_DETAILS,
FALSE_POSITIVES_DETAILS,
getDetails,
removeExternalLinkText,
INDEX_PATTERNS_DETAILS,
INVESTIGATION_NOTES_MARKDOWN,
@ -43,6 +42,7 @@ import {
TIMELINE_TEMPLATE_DETAILS,
} from '../../screens/rule_details';
import { getDetails } from '../../tasks/rule_details';
import {
changeRowsPerPageTo100,
filterByCustomRules,

View file

@ -35,7 +35,6 @@ import {
CUSTOM_QUERY_DETAILS,
DEFINITION_DETAILS,
FALSE_POSITIVES_DETAILS,
getDetails,
INDEX_PATTERNS_DETAILS,
INDICATOR_INDEX_PATTERNS,
INDICATOR_INDEX_QUERY,
@ -110,7 +109,7 @@ import {
import { goBackToRuleDetails, waitForKibana } from '../../tasks/edit_rule';
import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver';
import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { goBackToAllRulesTable } from '../../tasks/rule_details';
import { goBackToAllRulesTable, getDetails } from '../../tasks/rule_details';
import { ALERTS_URL, RULE_CREATION } from '../../urls/navigation';
const DEFAULT_THREAT_MATCH_QUERY = '@timestamp >= "now-30d/d"';

View file

@ -24,7 +24,6 @@ import {
ANOMALY_SCORE_DETAILS,
DEFINITION_DETAILS,
FALSE_POSITIVES_DETAILS,
getDetails,
removeExternalLinkText,
MACHINE_LEARNING_JOB_ID,
MACHINE_LEARNING_JOB_STATUS,
@ -40,6 +39,7 @@ import {
TIMELINE_TEMPLATE_DETAILS,
} from '../../screens/rule_details';
import { getDetails } from '../../tasks/rule_details';
import {
changeRowsPerPageTo100,
filterByCustomRules,

View file

@ -34,7 +34,6 @@ import {
DETAILS_DESCRIPTION,
DETAILS_TITLE,
FALSE_POSITIVES_DETAILS,
getDetails,
removeExternalLinkText,
INDEX_PATTERNS_DETAILS,
INVESTIGATION_NOTES_MARKDOWN,
@ -70,6 +69,7 @@ import {
waitForTheRuleToBeExecuted,
} from '../../tasks/create_new_rule';
import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { getDetails } from '../../tasks/rule_details';
import { RULE_CREATION } from '../../urls/navigation';

View file

@ -33,7 +33,6 @@ import {
CUSTOM_QUERY_DETAILS,
FALSE_POSITIVES_DETAILS,
DEFINITION_DETAILS,
getDetails,
removeExternalLinkText,
INDEX_PATTERNS_DETAILS,
INVESTIGATION_NOTES_MARKDOWN,
@ -51,6 +50,7 @@ import {
TIMELINE_TEMPLATE_DETAILS,
} from '../../screens/rule_details';
import { getDetails } from '../../tasks/rule_details';
import { goToManageAlertsDetectionRules } from '../../tasks/alerts';
import {
changeRowsPerPageTo100,

View file

@ -43,7 +43,7 @@ describe('Exceptions flyout', () => {
before(() => {
cleanKibana();
loginAndWaitForPageWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL);
createCustomRule(getNewRule());
createCustomRule({ ...getNewRule(), index: ['exceptions-*'] });
reload();
goToRuleDetails();

View file

@ -35,7 +35,7 @@ describe('From alert', () => {
beforeEach(() => {
cleanKibana();
loginAndWaitForPageWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL);
createCustomRule(getNewRule(), 'rule_testing', '10s');
createCustomRule({ ...getNewRule(), index: ['exceptions-*'] }, 'rule_testing', '10s');
reload();
goToRuleDetails();

View file

@ -35,7 +35,7 @@ describe('From rule', () => {
beforeEach(() => {
cleanKibana();
loginAndWaitForPageWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL);
createCustomRule(getNewRule(), 'rule_testing', '10s');
createCustomRule({ ...getNewRule(), index: ['exceptions-*'] }, 'rule_testing', '10s');
reload();
goToRuleDetails();

View file

@ -467,7 +467,7 @@ export const expectedExportedRule = (ruleResponse: Cypress.Response<RulesSchema>
immutable: false,
type: 'query',
language: 'kuery',
index: ['exceptions-*'],
index: getIndexPatterns(),
query,
throttle: 'no_actions',
actions: [],

View file

@ -7,6 +7,8 @@
export const BULK_ACTIONS_BTN = '[data-test-subj="bulkActions"] span';
export const BULK_ACTIONS_PROGRESS_BTN = '[data-test-subj="bulkActions-progress"]';
export const CREATE_NEW_RULE_BTN = '[data-test-subj="create-new-rule"]';
export const COLLAPSED_ACTION_BTN = '[data-test-subj="euiCollapsedItemActionsButton"]';
@ -39,6 +41,8 @@ export const FOURTH_RULE = 3;
export const LOAD_PREBUILT_RULES_BTN = '[data-test-subj="load-prebuilt-rules"]';
export const LOAD_PREBUILT_RULES_ON_PAGE_HEADER_BTN = '[data-test-subj="loadPrebuiltRulesBtn"]';
export const RULES_TABLE_INITIAL_LOADING_INDICATOR =
'[data-test-subj="initialLoadingPanelAllRulesTable"]';
@ -88,6 +92,10 @@ export const RULES_DELETE_CONFIRMATION_MODAL = '[data-test-subj="allRulesDeleteC
export const MODAL_CONFIRMATION_BTN = '[data-test-subj="confirmModalConfirmButton"]';
export const MODAL_CONFIRMATION_TITLE = '[data-test-subj="confirmModalTitleText"]';
export const MODAL_CONFIRMATION_BODY = '[data-test-subj="confirmModalBodyText"]';
export const RULE_DETAILS_DELETE_BTN = '[data-test-subj="rules-details-delete-rule"]';
export const ALERT_DETAILS_CELLS = '[data-test-subj="dataGridRowCell"]';
@ -104,7 +112,15 @@ export const INPUT_FILE = 'input[type=file]';
export const TOASTER = '[data-test-subj="euiToastHeader"]';
export const TOASTER_BODY = '[data-test-subj="globalToastList"] .euiToastBody';
export const RULE_IMPORT_OVERWRITE_CHECKBOX = '[id="import-data-modal-checkbox-label"]';
export const RULE_IMPORT_OVERWRITE_EXCEPTIONS_CHECKBOX =
'[id="import-data-modal-exceptions-checkbox-label"]';
export const RULES_TAGS_POPOVER_BTN = '[data-test-subj="tagsDisplayPopoverButton"]';
export const RULES_TAGS_POPOVER_WRAPPER = '[data-test-subj="tagsDisplayPopoverWrapper"]';
export const RULES_TAGS_FILTER_BTN = '[data-test-subj="tags-filter-popover-button"]';

View file

@ -97,9 +97,6 @@ export const TIMELINE_FIELD = (field: string) => {
return `[data-test-subj="formatted-field-${field}"]`;
};
export const getDetails = (title: string) =>
cy.get(DETAILS_TITLE).contains(title).next(DETAILS_DESCRIPTION);
export const removeExternalLinkText = (str: string) =>
str.replace(/\(opens in a new tab or window\)/g, '');

View file

@ -0,0 +1,39 @@
/*
* 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.
*/
export const INDEX_PATTERNS_RULE_BULK_MENU_ITEM = '[data-test-subj="indexPatternsBulkEditRule"]';
export const ADD_INDEX_PATTERNS_RULE_BULK_MENU_ITEM =
'[data-test-subj="addIndexPatternsBulkEditRule"]';
export const DELETE_INDEX_PATTERNS_RULE_BULK_MENU_ITEM =
'[data-test-subj="deleteIndexPatternsBulkEditRule"]';
export const TAGS_RULE_BULK_MENU_ITEM = '[data-test-subj="tagsBulkEditRule"]';
export const ADD_TAGS_RULE_BULK_MENU_ITEM = '[data-test-subj="addTagsBulkEditRule"]';
export const DELETE_TAGS_RULE_BULK_MENU_ITEM = '[data-test-subj="deleteTagsBulkEditRule"]';
export const RULES_BULK_EDIT_FORM_TITLE = '[data-test-subj="rulesBulkEditFormTitle"]';
export const RULES_BULK_EDIT_FORM_CONFIRM_BTN = '[data-test-subj="rulesBulkEditFormSaveBtn"]';
export const RULES_BULK_EDIT_INDEX_PATTERNS = '[data-test-subj="bulkEditRulesIndexPatterns"]';
export const RULES_BULK_EDIT_OVERWRITE_INDEX_PATTERNS_CHECKBOX =
'[data-test-subj="bulkEditRulesOverwriteIndexPatterns"]';
export const RULES_BULK_EDIT_TAGS = '[data-test-subj="bulkEditRulesTags"]';
export const RULES_BULK_EDIT_OVERWRITE_TAGS_CHECKBOX =
'[data-test-subj="bulkEditRulesOverwriteTags"]';
export const RULES_BULK_EDIT_INDEX_PATTERNS_WARNING =
'[data-test-subj="bulkEditRulesIndexPatternsWarning"]';
export const RULES_BULK_EDIT_TAGS_WARNING = '[data-test-subj="bulkEditRulesTagsWarning"]';

View file

@ -45,6 +45,8 @@ import {
TOASTER,
RULE_IMPORT_OVERWRITE_CHECKBOX,
RULE_IMPORT_OVERWRITE_EXCEPTIONS_CHECKBOX,
RULES_TAGS_POPOVER_BTN,
RULES_TAGS_POPOVER_WRAPPER,
} from '../screens/alerts_detection_rules';
import { ALL_ACTIONS } from '../screens/rule_details';
import { LOADING_INDICATOR } from '../screens/security_header';
@ -290,3 +292,13 @@ export const importRulesWithOverwriteAll = (rulesFile: string) => {
cy.get(RULE_IMPORT_MODAL_BUTTON).last().click({ force: true });
cy.get(INPUT_FILE).should('not.exist');
};
export const testAllTagsBadges = (tags: string[]) => {
cy.get(RULES_TAGS_POPOVER_BTN).each(($el) => {
// open tags popover
cy.wrap($el).click();
cy.get(RULES_TAGS_POPOVER_WRAPPER).should('have.text', tags.join(''));
// close tags popover
cy.wrap($el).click();
});
};

View file

@ -20,7 +20,7 @@ export const createCustomRule = (rule: CustomRule, ruleId = 'rule_testing', inte
severity: rule.severity.toLocaleLowerCase(),
type: 'query',
from: 'now-50000h',
index: ['exceptions-*'],
index: rule.index,
query: rule.customQuery,
language: 'kuery',
enabled: false,

View file

@ -24,6 +24,10 @@ import {
REFRESH_BUTTON,
REMOVE_EXCEPTION_BTN,
RULE_SWITCH,
DEFINITION_DETAILS,
INDEX_PATTERNS_DETAILS,
DETAILS_TITLE,
DETAILS_DESCRIPTION,
} from '../screens/rule_details';
import { addsFields, closeFieldsBrowser, filterFieldsBrowser } from './fields_browser';
@ -107,3 +111,12 @@ export const waitForTheRuleToBeExecuted = () => {
export const goBackToAllRulesTable = () => {
cy.get(BACK_TO_RULES).click();
};
export const getDetails = (title: string) =>
cy.get(DETAILS_TITLE).contains(title).next(DETAILS_DESCRIPTION);
export const hasIndexPatterns = (indexPatterns: string) => {
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns);
});
};

View file

@ -0,0 +1,104 @@
/*
* 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 {
BULK_ACTIONS_BTN,
BULK_ACTIONS_PROGRESS_BTN,
MODAL_CONFIRMATION_TITLE,
MODAL_CONFIRMATION_BODY,
TOASTER_BODY,
} from '../screens/alerts_detection_rules';
import {
INDEX_PATTERNS_RULE_BULK_MENU_ITEM,
ADD_INDEX_PATTERNS_RULE_BULK_MENU_ITEM,
DELETE_INDEX_PATTERNS_RULE_BULK_MENU_ITEM,
TAGS_RULE_BULK_MENU_ITEM,
ADD_TAGS_RULE_BULK_MENU_ITEM,
DELETE_TAGS_RULE_BULK_MENU_ITEM,
RULES_BULK_EDIT_FORM_TITLE,
RULES_BULK_EDIT_INDEX_PATTERNS,
RULES_BULK_EDIT_TAGS,
RULES_BULK_EDIT_FORM_CONFIRM_BTN,
} from '../screens/rules_bulk_edit';
export const clickAddIndexPatternsMenuItem = () => {
cy.get(BULK_ACTIONS_BTN).click();
cy.get(INDEX_PATTERNS_RULE_BULK_MENU_ITEM).click();
cy.get(ADD_INDEX_PATTERNS_RULE_BULK_MENU_ITEM).click();
};
export const openBulkEditAddIndexPatternsForm = () => {
clickAddIndexPatternsMenuItem();
cy.get(RULES_BULK_EDIT_FORM_TITLE).should('have.text', 'Add index patterns');
};
export const openBulkEditDeleteIndexPatternsForm = () => {
cy.get(BULK_ACTIONS_BTN).click();
cy.get(INDEX_PATTERNS_RULE_BULK_MENU_ITEM).click();
cy.get(DELETE_INDEX_PATTERNS_RULE_BULK_MENU_ITEM).click();
cy.get(RULES_BULK_EDIT_FORM_TITLE).should('have.text', 'Delete index patterns');
};
export const openBulkEditAddTagsForm = () => {
cy.get(BULK_ACTIONS_BTN).click();
cy.get(TAGS_RULE_BULK_MENU_ITEM).click();
cy.get(ADD_TAGS_RULE_BULK_MENU_ITEM).click();
cy.get(RULES_BULK_EDIT_FORM_TITLE).should('have.text', 'Add tags');
};
export const openBulkEditDeleteTagsForm = () => {
cy.get(BULK_ACTIONS_BTN).click();
cy.get(TAGS_RULE_BULK_MENU_ITEM).click();
cy.get(DELETE_TAGS_RULE_BULK_MENU_ITEM).click();
cy.get(RULES_BULK_EDIT_FORM_TITLE).should('have.text', 'Delete tags');
};
export const typeIndexPatterns = (indices: string[]) => {
cy.get(RULES_BULK_EDIT_INDEX_PATTERNS).find('input').type(indices.join('{enter}'));
};
export const typeTags = (tags: string[]) => {
cy.get(RULES_BULK_EDIT_TAGS).find('input').type(tags.join('{enter}'));
};
export const confirmBulkEditForm = () => cy.get(RULES_BULK_EDIT_FORM_CONFIRM_BTN).click();
export const waitForBulkEditActionToFinish = ({ rulesCount }: { rulesCount: number }) => {
cy.get(BULK_ACTIONS_PROGRESS_BTN).should('be.disabled');
cy.contains(TOASTER_BODY, `Youve successfully updated ${rulesCount} rule`);
};
export const waitForElasticRulesBulkEditModal = (rulesCount: number) => {
cy.get(MODAL_CONFIRMATION_TITLE).should(
'have.text',
`${rulesCount} Elastic rules cannot be edited`
);
cy.get(MODAL_CONFIRMATION_BODY).should(
'have.text',
'Elastic rules are not modifiable. The update action will only be applied to Custom rules.'
);
};
export const waitForMixedRulesBulkEditModal = (
elasticRulesCount: number,
customRulesCount: number
) => {
cy.get(MODAL_CONFIRMATION_TITLE).should(
'have.text',
`${elasticRulesCount} Elastic rules cannot be edited`
);
cy.get(MODAL_CONFIRMATION_BODY).should(
'have.text',
`The update action will only be applied to ${customRulesCount} Custom rules youve selected.`
);
};

View file

@ -23,7 +23,6 @@ import {
ABOUT_RULE_DESCRIPTION,
CUSTOM_QUERY_DETAILS,
DEFINITION_DETAILS,
getDetails,
INDEX_PATTERNS_DETAILS,
RISK_SCORE_DETAILS,
RULE_NAME_HEADER,
@ -34,6 +33,7 @@ import {
TIMELINE_TEMPLATE_DETAILS,
} from '../../../screens/rule_details';
import { getDetails } from '../../../tasks/rule_details';
import { waitForPageToBeLoaded } from '../../../tasks/common';
import {
waitForRulesTableToBeLoaded,

View file

@ -13,7 +13,6 @@ import {
ABOUT_RULE_DESCRIPTION,
CUSTOM_QUERY_DETAILS,
DEFINITION_DETAILS,
getDetails,
INDEX_PATTERNS_DETAILS,
RISK_SCORE_DETAILS,
RULE_NAME_HEADER,
@ -25,6 +24,7 @@ import {
TIMELINE_TEMPLATE_DETAILS,
} from '../../../screens/rule_details';
import { getDetails } from '../../../tasks/rule_details';
import { expandFirstAlert } from '../../../tasks/alerts';
import { waitForPageToBeLoaded } from '../../../tasks/common';
import {

View file

@ -101,8 +101,14 @@ export const UtilityBarAction = React.memo<UtilityBarActionProps>(
}) => {
if (inProgress) {
return (
<BarAction data-test-subj={dataTestSubj}>
<LoadingButtonEmpty size="xs" className="eui-alignTop" isLoading iconSide="right">
<BarAction>
<LoadingButtonEmpty
data-test-subj={`${dataTestSubj}-progress`}
size="xs"
className="eui-alignTop"
isLoading
iconSide="right"
>
{children}
</LoadingButtonEmpty>
</BarAction>

View file

@ -39,14 +39,14 @@ const BulkEditFormWrapperComponent: FC<BulkEditFormWrapperProps> = ({
title,
}) => {
const simpleFlyoutTitleId = useGeneratedHtmlId({
prefix: 'BulkEditForm',
prefix: 'RulesBulkEditForm',
});
const { isValid } = form;
return (
<EuiFlyout ownFocus onClose={onClose} aria-labelledby={simpleFlyoutTitleId} size="s">
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<EuiTitle size="m" data-test-subj="rulesBulkEditFormTitle">
<h2 id={simpleFlyoutTitleId}>{title}</h2>
</EuiTitle>
</EuiFlyoutHeader>
@ -56,12 +56,22 @@ const BulkEditFormWrapperComponent: FC<BulkEditFormWrapperProps> = ({
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" onClick={onClose} flush="left">
<EuiButtonEmpty
iconType="cross"
onClick={onClose}
flush="left"
data-test-subj="rulesBulkEditFormCancelBtn"
>
{i18n.BULK_EDIT_FLYOUT_FORM_CLOSE}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton onClick={onSubmit} fill disabled={isValid === false}>
<EuiButton
onClick={onSubmit}
fill
disabled={isValid === false}
data-test-subj="rulesBulkEditFormSaveBtn"
>
{i18n.BULK_EDIT_FLYOUT_FORM_SAVE}
</EuiButton>
</EuiFlexItem>

View file

@ -79,7 +79,7 @@ interface IndexPatternsFormProps {
editAction: IndexPatternsEditActions;
rulesCount: number;
onClose: () => void;
onConfirm: (bulkactionEditPayload: BulkActionEditPayload) => void;
onConfirm: (bulkActionEditPayload: BulkActionEditPayload) => void;
}
const IndexPatternsFormComponent = ({
@ -119,8 +119,8 @@ const IndexPatternsFormComponent = ({
path="index"
config={{ ...schema.index, label: indexLabel, helpText: indexHelpText }}
componentProps={{
idAria: 'detectionEngineBulkEditIndexPatterns',
'data-test-subj': 'detectionEngineBulkEditIndexPatterns',
idAria: 'bulkEditRulesIndexPatterns',
'data-test-subj': 'bulkEditRulesIndexPatterns',
euiFieldProps: {
fullWidth: true,
placeholder: '',
@ -133,14 +133,14 @@ const IndexPatternsFormComponent = ({
<CommonUseField
path="overwrite"
componentProps={{
idAria: 'detectionEngineBulkEditOverwriteIndexPatterns',
'data-test-subj': 'detectionEngineBulkEditOverwriteIndexPatterns',
idAria: 'bulkEditRulesOverwriteIndexPatterns',
'data-test-subj': 'bulkEditRulesOverwriteIndexPatterns',
}}
/>
)}
{overwrite && (
<EuiFormRow>
<EuiCallOut color="warning" size="s">
<EuiCallOut color="warning" size="s" data-test-subj="bulkEditRulesIndexPatternsWarning">
<FormattedMessage
id="xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.setIndexPatternsWarningCallout"
defaultMessage="Youre about to overwrite index patterns for {rulesCount, plural, one {# selected rule} other {# selected rules}}, press Save to

View file

@ -75,7 +75,7 @@ interface TagsFormProps {
editAction: TagsEditActions;
rulesCount: number;
onClose: () => void;
onConfirm: (bulkactionEditPayload: BulkActionEditPayload) => void;
onConfirm: (bulkActionEditPayload: BulkActionEditPayload) => void;
tags: string[];
}
@ -109,8 +109,8 @@ const TagsFormComponent = ({ editAction, rulesCount, onClose, onConfirm, tags }:
path="tags"
config={{ ...schema.tags, label: tagsLabel, helpText: tagsHelpText }}
componentProps={{
idAria: 'detectionEngineBulkEditTags',
'data-test-subj': 'detectionEngineBulkEditTags',
idAria: 'bulkEditRulesTags',
'data-test-subj': 'bulkEditRulesTags',
euiFieldProps: {
fullWidth: true,
placeholder: '',
@ -123,14 +123,14 @@ const TagsFormComponent = ({ editAction, rulesCount, onClose, onConfirm, tags }:
<CommonUseField
path="overwrite"
componentProps={{
idAria: 'detectionEngineBulkEditOverwriteTags',
'data-test-subj': 'detectionEngineBulkEditOverwriteTags',
idAria: 'bulkEditRulesOverwriteTags',
'data-test-subj': 'bulkEditRulesOverwriteTags',
}}
/>
) : null}
{overwrite && (
<EuiFormRow>
<EuiCallOut color="warning" size="s">
<EuiCallOut color="warning" size="s" data-test-subj="bulkEditRulesTagsWarning">
<FormattedMessage
id="xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.setTagsWarningCallout"
defaultMessage="Youre about to overwrite tags for {rulesCount, plural, one {# selected rule} other {# selected rules}}, press Save to

View file

@ -224,124 +224,112 @@ export const useBulkActions = ({
const handleBulkEdit = (bulkEditActionType: BulkActionEditType) => async () => {
let longTimeWarningToast: Toast;
let isBulkEditFinished = false;
try {
// disabling auto-refresh so user's selected rules won't disappear after table refresh
setIsRefreshOn(false);
closePopover();
const customSelectedRuleIds = selectedRules
.filter((rule) => rule.immutable === false)
.map((rule) => rule.id);
// disabling auto-refresh so user's selected rules won't disappear after table refresh
setIsRefreshOn(false);
closePopover();
// User has cancelled edit action or there are no custom rules to proceed
if ((await confirmBulkEdit()) === false) {
setIsRefreshOn(true);
const customSelectedRuleIds = selectedRules
.filter((rule) => rule.immutable === false)
.map((rule) => rule.id);
// User has cancelled edit action or there are no custom rules to proceed
if ((await confirmBulkEdit()) === false) {
setIsRefreshOn(true);
return;
}
const editPayload = await completeBulkEditForm(bulkEditActionType);
if (editPayload == null) {
setIsRefreshOn(true);
return;
}
const hideWarningToast = () => {
if (longTimeWarningToast) {
toasts.api.remove(longTimeWarningToast);
}
};
const customRulesCount = isAllSelected
? getCustomRulesCountFromCache(queryClient)
: customSelectedRuleIds.length;
// show warning toast only if bulk edit action exceeds 5s
// if bulkAction already finished, we won't show toast at all (hence flag "isBulkEditFinished")
setTimeout(() => {
if (isBulkEditFinished) {
return;
}
const editPayload = await completeBulkEditForm(bulkEditActionType);
if (editPayload == null) {
throw Error('Bulk edit payload is empty');
}
const hideWarningToast = () => {
if (longTimeWarningToast) {
toasts.api.remove(longTimeWarningToast);
}
};
const customRulesCount = isAllSelected
? getCustomRulesCountFromCache(queryClient)
: customSelectedRuleIds.length;
// show warning toast only if bulk edit action exceeds 5s
// if bulkAction already finished, we won't show toast at all (hence flag "isBulkEditFinished")
setTimeout(() => {
if (isBulkEditFinished) {
return;
}
longTimeWarningToast = toasts.addWarning(
{
title: i18n.BULK_EDIT_WARNING_TOAST_TITLE,
text: mountReactNode(
<>
<p>{i18n.BULK_EDIT_WARNING_TOAST_DESCRIPTION(customRulesCount)}</p>
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiButton color="warning" size="s" onClick={hideWarningToast}>
{i18n.BULK_EDIT_WARNING_TOAST_NOTIFY}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</>
),
iconType: undefined,
},
{ toastLifeTimeMs: 10 * 60 * 1000 }
);
}, 5 * 1000);
const rulesBulkAction = initRulesBulkAction({
visibleRuleIds: selectedRuleIds,
action: BulkAction.edit,
setLoadingRules,
toasts,
payload: { edit: [editPayload] },
onSuccess: ({ rulesCount }) => {
hideWarningToast();
toasts.addSuccess({
title: i18n.BULK_EDIT_SUCCESS_TOAST_TITLE,
text: i18n.BULK_EDIT_SUCCESS_TOAST_DESCRIPTION(rulesCount),
iconType: undefined,
});
longTimeWarningToast = toasts.addWarning(
{
title: i18n.BULK_EDIT_WARNING_TOAST_TITLE,
text: mountReactNode(
<>
<p>{i18n.BULK_EDIT_WARNING_TOAST_DESCRIPTION(customRulesCount)}</p>
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiButton color="warning" size="s" onClick={hideWarningToast}>
{i18n.BULK_EDIT_WARNING_TOAST_NOTIFY}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</>
),
iconType: undefined,
},
onError: (error: HTTPError) => {
hideWarningToast();
{ toastLifeTimeMs: 10 * 60 * 1000 }
);
}, 5 * 1000);
// if response doesn't have number of failed rules, it means the whole bulk action failed
// and general error toast will be shown. Otherwise - error toast for partial failure
const failedRulesCount = (error?.body as BulkActionPartialErrorResponse)?.attributes
?.rules?.failed;
if (isNaN(failedRulesCount)) {
toasts.addError(error, { title: i18n.BULK_ACTION_FAILED });
} else {
try {
error.stack = JSON.stringify(error.body, null, 2);
toasts.addError(error, {
title: i18n.BULK_EDIT_ERROR_TOAST_TITLE,
toastMessage: i18n.BULK_EDIT_ERROR_TOAST_DESCRIPTION(failedRulesCount),
});
} catch (e) {
// toast error has failed
}
}
},
});
// only edit custom rules, as elastic rule are immutable
if (isAllSelected) {
const customRulesOnlyFilterQuery = convertRulesFilterToKQL({
...filterOptions,
showCustomRules: true,
const rulesBulkAction = initRulesBulkAction({
visibleRuleIds: selectedRuleIds,
action: BulkAction.edit,
setLoadingRules,
toasts,
payload: { edit: [editPayload] },
onSuccess: ({ rulesCount }) => {
hideWarningToast();
toasts.addSuccess({
title: i18n.BULK_EDIT_SUCCESS_TOAST_TITLE,
text: i18n.BULK_EDIT_SUCCESS_TOAST_DESCRIPTION(rulesCount),
iconType: undefined,
});
await rulesBulkAction.byQuery(customRulesOnlyFilterQuery);
} else {
await rulesBulkAction.byIds(customSelectedRuleIds);
}
},
onError: (error: HTTPError) => {
hideWarningToast();
// if response doesn't have number of failed rules, it means the whole bulk action failed
// and general error toast will be shown. Otherwise - error toast for partial failure
const failedRulesCount = (error?.body as BulkActionPartialErrorResponse)?.attributes
?.rules?.failed;
invalidateRules();
isBulkEditFinished = true;
if (getIsMounted()) {
await resolveTagsRefetch(bulkEditActionType);
}
} catch (e) {
// user has cancelled form or error has occured
} finally {
isBulkEditFinished = true;
if (getIsMounted()) {
setIsRefreshOn(true);
}
if (isNaN(failedRulesCount)) {
toasts.addError(error, { title: i18n.BULK_ACTION_FAILED });
} else {
error.stack = JSON.stringify(error.body, null, 2);
toasts.addError(error, {
title: i18n.BULK_EDIT_ERROR_TOAST_TITLE,
toastMessage: i18n.BULK_EDIT_ERROR_TOAST_DESCRIPTION(failedRulesCount),
});
}
},
});
// only edit custom rules, as elastic rule are immutable
if (isAllSelected) {
const customRulesOnlyFilterQuery = convertRulesFilterToKQL({
...filterOptions,
showCustomRules: true,
});
await rulesBulkAction.byQuery(customRulesOnlyFilterQuery);
} else {
await rulesBulkAction.byIds(customSelectedRuleIds);
}
isBulkEditFinished = true;
invalidateRules();
if (getIsMounted()) {
await resolveTagsRefetch(bulkEditActionType);
}
};

View file

@ -29,7 +29,7 @@ export const useBulkEditFormFlyout = () => {
if ((await confirmForm()) === true) {
return dataFormRef.current;
} else {
throw Error('Form is cancelled');
return null;
}
},
[confirmForm]

View file

@ -17,20 +17,20 @@ import {
} from '../__mocks__/request_responses';
import { requestContextMock, serverMock, requestMock } from '../__mocks__';
import { performBulkActionRoute } from './perform_bulk_action_route';
import { getPerformBulkActionSchemaMock } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema.mock';
import {
getPerformBulkActionSchemaMock,
getPerformBulkActionEditSchemaMock,
} from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema.mock';
import { loggingSystemMock } from 'src/core/server/mocks';
import { isElasticRule } from '../../../../usage/detections';
import { readRules } from '../../rules/read_rules';
jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create());
jest.mock('../../../../usage/detections', () => ({ isElasticRule: jest.fn() }));
jest.mock('../../rules/read_rules', () => ({ readRules: jest.fn() }));
describe.each([
['Legacy', false],
['RAC', true],
])('perform_bulk_action - %s', (_, isRuleRegistryEnabled) => {
const isElasticRuleMock = isElasticRule as jest.Mock;
const readRulesMock = readRules as jest.Mock;
let server: ReturnType<typeof serverMock.create>;
let { clients, context } = requestContextMock.createTools();
@ -43,9 +43,7 @@ describe.each([
logger = loggingSystemMock.createLogger();
({ clients, context } = requestContextMock.createTools());
ml = mlServicesMock.createSetupContract();
isElasticRuleMock.mockReturnValue(false);
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit(isRuleRegistryEnabled));
performBulkActionRoute(server.router, ml, logger, isRuleRegistryEnabled);
});
@ -78,10 +76,9 @@ describe.each([
describe('rules execution failures', () => {
it('returns error if rule is immutable/elastic', async () => {
isElasticRuleMock.mockReturnValue(true);
clients.rulesClient.find.mockResolvedValue(
getFindResultWithMultiHits({
data: [mockRule],
data: [{ ...mockRule, params: { ...mockRule.params, immutable: true } }],
total: 1,
})
);
@ -179,7 +176,106 @@ describe.each([
});
});
it('returns partial failure error if couple of rule validations fail and the rest are successfull', async () => {
it('returns error if index patterns action is applied to machine learning rule', async () => {
readRulesMock.mockImplementationOnce(() =>
Promise.resolve({ ...mockRule, params: { ...mockRule.params, type: 'machine_learning' } })
);
const request = requestMock.create({
method: 'patch',
path: DETECTION_ENGINE_RULES_BULK_ACTION,
body: {
...getPerformBulkActionEditSchemaMock(),
ids: ['failed-mock-id'],
query: undefined,
edit: [
{
type: 'add_index_patterns',
value: ['new-index-*'],
},
],
},
});
const response = await server.inject(request, context);
expect(response.status).toEqual(500);
expect(response.body).toEqual({
attributes: {
rules: {
failed: 1,
succeeded: 0,
total: 1,
},
errors: [
{
message:
"Index patterns can't be added. Machine learning rule doesn't have index patterns property",
status_code: 500,
rules: [
{
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
name: 'Detect Root/Admin Users',
},
],
},
],
},
message: 'Bulk edit failed',
status_code: 500,
});
});
it('returns error if all index pattern tried to be deleted', async () => {
readRulesMock.mockImplementationOnce(() =>
Promise.resolve({ ...mockRule, params: { ...mockRule.params, index: ['index-*'] } })
);
const request = requestMock.create({
method: 'patch',
path: DETECTION_ENGINE_RULES_BULK_ACTION,
body: {
...getPerformBulkActionEditSchemaMock(),
ids: ['failed-mock-id'],
query: undefined,
edit: [
{
type: 'delete_index_patterns',
value: ['index-*'],
},
],
},
});
const response = await server.inject(request, context);
expect(response.status).toEqual(500);
expect(response.body).toEqual({
attributes: {
rules: {
failed: 1,
succeeded: 0,
total: 1,
},
errors: [
{
message: "Can't delete all index patterns. At least one index pattern must be left",
status_code: 500,
rules: [
{
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
name: 'Detect Root/Admin Users',
},
],
},
],
},
message: 'Bulk edit failed',
status_code: 500,
});
});
it('returns partial failure error if couple of rule validations fail and the rest are successful', async () => {
clients.rulesClient.find.mockResolvedValue(
getFindResultWithMultiHits({
data: [

View file

@ -25,7 +25,6 @@ import type { SecuritySolutionPluginRouter } from '../../../../types';
import { buildRouteValidation } from '../../../../utils/build_validation/route_validation';
import { routeLimitedConcurrencyTag } from '../../../../utils/route_limited_concurrency_tag';
import { initPromisePool } from '../../../../utils/promise_pool';
import { isElasticRule } from '../../../../usage/detections';
import { buildMlAuthz } from '../../../machine_learning/authz';
import { throwHttpError } from '../../../machine_learning/validation';
import { deleteRules } from '../../rules/delete_rules';
@ -364,7 +363,7 @@ export const performBulkActionRoute = (
rules,
async (rule) => {
throwHttpError({
valid: !isElasticRule(rule.tags),
valid: !rule.params.immutable,
message: 'Elastic rule can`t be edited',
});

View file

@ -80,11 +80,14 @@ describe('bulk_action_edit', () => {
expect(editedRule.params).toHaveProperty('index', ['initial-index-*', 'my-index-*']);
});
test('should remove index pattern from rule', () => {
const editedRule = applyBulkActionEditToRule(ruleMock as RuleAlertType, {
type: BulkActionEditType.delete_index_patterns,
value: ['initial-index-*'],
});
expect(editedRule.params).toHaveProperty('index', []);
const editedRule = applyBulkActionEditToRule(
{ params: { index: ['initial-index-*', 'index-2-*'] } } as RuleAlertType,
{
type: BulkActionEditType.delete_index_patterns,
value: ['index-2-*'],
}
);
expect(editedRule.params).toHaveProperty('index', ['initial-index-*']);
});
test('should rewrite index pattern in rule', () => {
@ -95,28 +98,55 @@ describe('bulk_action_edit', () => {
expect(editedRule.params).toHaveProperty('index', ['index']);
});
test('should not add new index pattern to rule if index pattern is absent', () => {
const editedRule = applyBulkActionEditToRule({ params: {} } as RuleAlertType, {
type: BulkActionEditType.add_index_patterns,
value: ['my-index-*'],
});
expect(editedRule.params).not.toHaveProperty('index');
test('should throw error on adding index pattern if rule is of machine learning type', () => {
expect(() =>
applyBulkActionEditToRule({ params: { type: 'machine_learning' } } as RuleAlertType, {
type: BulkActionEditType.add_index_patterns,
value: ['my-index-*'],
})
).toThrow(
"Index patterns can't be added. Machine learning rule doesn't have index patterns property"
);
});
test('should not remove index pattern to rule if index pattern is absent', () => {
const editedRule = applyBulkActionEditToRule({ params: {} } as RuleAlertType, {
type: BulkActionEditType.delete_index_patterns,
value: ['initial-index-*'],
});
expect(editedRule.params).not.toHaveProperty('index');
test('should throw error on deleting index pattern if rule is of machine learning type', () => {
expect(() =>
applyBulkActionEditToRule({ params: { type: 'machine_learning' } } as RuleAlertType, {
type: BulkActionEditType.delete_index_patterns,
value: ['my-index-*'],
})
).toThrow(
"Index patterns can't be deleted. Machine learning rule doesn't have index patterns property"
);
});
test('should not set index pattern to rule if index pattern is absent', () => {
const editedRule = applyBulkActionEditToRule({ params: {} } as RuleAlertType, {
type: BulkActionEditType.set_index_patterns,
value: ['index-*'],
});
expect(editedRule.params).not.toHaveProperty('index');
test('should throw error on overwriting index pattern if rule is of machine learning type', () => {
expect(() =>
applyBulkActionEditToRule({ params: { type: 'machine_learning' } } as RuleAlertType, {
type: BulkActionEditType.set_index_patterns,
value: ['my-index-*'],
})
).toThrow(
"Index patterns can't be overwritten. Machine learning rule doesn't have index patterns property"
);
});
test('should throw error if all index patterns are deleted', () => {
expect(() =>
applyBulkActionEditToRule({ params: { index: ['my-index-*'] } } as RuleAlertType, {
type: BulkActionEditType.delete_index_patterns,
value: ['my-index-*'],
})
).toThrow("Can't delete all index patterns. At least one index pattern must be left");
});
test('should throw error if all index patterns are rewritten with empty list', () => {
expect(() =>
applyBulkActionEditToRule({ params: { index: ['my-index-*'] } } as RuleAlertType, {
type: BulkActionEditType.set_index_patterns,
value: [],
})
).toThrow("Index patterns can't be overwritten with empty list");
});
});

View file

@ -12,6 +12,8 @@ import {
BulkActionEditType,
} from '../../../../common/detection_engine/schemas/common/schemas';
import { invariant } from '../../../../common/utils/invariant';
export const addItemsToArray = <T>(arr: T[], items: T[]): T[] =>
Array.from(new Set([...arr, ...items]));
@ -40,24 +42,38 @@ export const applyBulkActionEditToRule = (
break;
// index_patterns actions
// index is not present in all rule types(machine learning). But it's mandatory for the rest.
// So we check if index is present and only in that case apply action
// index pattern is not present in machine learning rule type, so we throw error on it
case BulkActionEditType.add_index_patterns:
if (rule.params && 'index' in rule.params) {
rule.params.index = addItemsToArray(rule.params.index ?? [], action.value);
}
invariant(
rule.params.type !== 'machine_learning',
"Index patterns can't be added. Machine learning rule doesn't have index patterns property"
);
rule.params.index = addItemsToArray(rule.params.index ?? [], action.value);
break;
case BulkActionEditType.delete_index_patterns:
if (rule.params && 'index' in rule.params) {
rule.params.index = deleteItemsFromArray(rule.params.index ?? [], action.value);
}
invariant(
rule.params.type !== 'machine_learning',
"Index patterns can't be deleted. Machine learning rule doesn't have index patterns property"
);
rule.params.index = deleteItemsFromArray(rule.params.index ?? [], action.value);
invariant(
rule.params.index.length !== 0,
"Can't delete all index patterns. At least one index pattern must be left"
);
break;
case BulkActionEditType.set_index_patterns:
if (rule.params && 'index' in rule.params) {
rule.params.index = action.value;
}
invariant(
rule.params.type !== 'machine_learning',
"Index patterns can't be overwritten. Machine learning rule doesn't have index patterns property"
);
invariant(action.value.length !== 0, "Index patterns can't be overwritten with empty list");
rule.params.index = action.value;
break;
// timeline actions