mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
5629decd9e
commit
ae51f810f5
30 changed files with 736 additions and 192 deletions
|
@ -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',
|
||||
'You’re 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',
|
||||
'You’re about to overwrite tags for 6 selected rules, press Save to apply changes.'
|
||||
);
|
||||
typeTags(['overwrite-tag']);
|
||||
confirmBulkEditForm();
|
||||
waitForBulkEditActionToFinish({ rulesCount: 6 });
|
||||
|
||||
testAllTagsBadges(['overwrite-tag']);
|
||||
});
|
||||
});
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -43,7 +43,7 @@ describe('Exceptions flyout', () => {
|
|||
before(() => {
|
||||
cleanKibana();
|
||||
loginAndWaitForPageWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL);
|
||||
createCustomRule(getNewRule());
|
||||
createCustomRule({ ...getNewRule(), index: ['exceptions-*'] });
|
||||
reload();
|
||||
goToRuleDetails();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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"]';
|
||||
|
|
|
@ -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, '');
|
||||
|
||||
|
|
|
@ -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"]';
|
|
@ -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();
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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, `You’ve 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 you’ve selected.`
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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="You’re about to overwrite index patterns for {rulesCount, plural, one {# selected rule} other {# selected rules}}, press Save to
|
||||
|
|
|
@ -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="You’re about to overwrite tags for {rulesCount, plural, one {# selected rule} other {# selected rules}}, press Save to
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ export const useBulkEditFormFlyout = () => {
|
|||
if ((await confirmForm()) === true) {
|
||||
return dataFormRef.current;
|
||||
} else {
|
||||
throw Error('Form is cancelled');
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[confirmForm]
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue