[Security solution][Detections] increases test coverage for bulk edit (#137165)

## Summary

- addresses https://github.com/elastic/kibana/issues/135201, apart from data views
- adds tests for scenarios in test plan: [internal document](https://docs.google.com/document/d/116x7ITTTJQ6cTiwaGK831_f6Ox7XB3qyLiHxC3Cmf8w/edit#)
- adds data-test-subj attributes to error toasts


### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
This commit is contained in:
Vitalii Dmyterko 2022-08-30 15:10:33 +01:00 committed by GitHub
parent 04d1ffea7e
commit eb525ce0b5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 1146 additions and 405 deletions

View file

@ -12,6 +12,7 @@ exports[`renders matching snapshot 1`] = `
>
<EuiButton
color="danger"
data-test-subj="errorToastBtn"
onClick={[Function]}
size="s"
>

View file

@ -73,7 +73,7 @@ function showErrorDialog({
<EuiModalHeader>
<EuiModalHeaderTitle>{title}</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiModalBody data-test-subj="errorModalBody">
<EuiCallOut size="s" color="danger" iconType="alert" title={error.message} />
{text && (
<React.Fragment>
@ -112,6 +112,7 @@ export function ErrorToast({
<EuiButton
size="s"
color="danger"
data-test-subj="errorToastBtn"
onClick={() => showErrorDialog({ title, error, openModal, i18nContext })}
>
<FormattedMessage

View file

@ -6,23 +6,27 @@
*/
import {
ELASTIC_RULES_BTN,
CUSTOM_RULES_BTN,
MODAL_CONFIRMATION_BTN,
SELECT_ALL_RULES_ON_PAGE_CHECKBOX,
RULES_TAGS_FILTER_BTN,
MODAL_CONFIRMATION_BODY,
RULE_CHECKBOX,
RULES_TAGS_POPOVER_BTN,
RULES_TABLE_REFRESH_INDICATOR,
TOASTER_BODY,
MODAL_ERROR_BODY,
} 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,
RULES_BULK_EDIT_TIMELINE_TEMPLATES_WARNING,
TAGS_RULE_BULK_MENU_ITEM,
INDEX_PATTERNS_RULE_BULK_MENU_ITEM,
APPLY_TIMELINE_RULE_BULK_MENU_ITEM,
} from '../../screens/rules_bulk_edit';
import { TIMELINE_TEMPLATE_DETAILS } from '../../screens/rule_details';
import { EUI_FILTER_SELECT_ITEM } from '../../screens/common/controls';
import {
changeRowsPerPageTo,
waitForRulesTableToBeLoaded,
@ -34,50 +38,86 @@ import {
testMultipleSelectedRulesLabel,
loadPrebuiltDetectionRulesFromHeaderBtn,
switchToElasticRules,
clickErrorToastBtn,
unselectRuleByName,
cancelConfirmationModal,
} from '../../tasks/alerts_detection_rules';
import {
openBulkEditAddIndexPatternsForm,
openBulkEditDeleteIndexPatternsForm,
typeIndexPatterns,
waitForBulkEditActionToFinish,
confirmBulkEditForm,
submitBulkEditForm,
clickAddIndexPatternsMenuItem,
checkElasticRulesCannotBeModified,
checkPrebuiltRulesCannotBeModified,
checkMachineLearningRulesCannotBeModified,
waitForMixedRulesBulkEditModal,
openBulkEditAddTagsForm,
openBulkEditDeleteTagsForm,
typeTags,
openTagsSelect,
openBulkActionsMenu,
clickApplyTimelineTemplatesMenuItem,
clickAddTagsMenuItem,
checkOverwriteTagsCheckbox,
checkOverwriteIndexPatternsCheckbox,
openBulkEditAddIndexPatternsForm,
openBulkEditDeleteIndexPatternsForm,
selectTimelineTemplate,
checkTagsInTagsFilter,
} from '../../tasks/rules_bulk_edit';
import { hasIndexPatterns } from '../../tasks/rule_details';
import { hasIndexPatterns, getDetails } from '../../tasks/rule_details';
import { login, visitWithoutDateRange } from '../../tasks/login';
import { SECURITY_DETECTIONS_RULES_URL } from '../../urls/navigation';
import { createCustomRule, createMachineLearningRule } from '../../tasks/api_calls/rules';
import { cleanKibana, deleteAlertsAndRules } from '../../tasks/common';
import {
getExistingRule,
getNewOverrideRule,
createCustomRule,
createMachineLearningRule,
createCustomIndicatorRule,
createEventCorrelationRule,
createThresholdRule,
createNewTermsRule,
} from '../../tasks/api_calls/rules';
import { loadPrepackagedTimelineTemplates } from '../../tasks/api_calls/timelines';
import { cleanKibana, deleteAlertsAndRules } from '../../tasks/common';
import {
getEqlRule,
getNewThreatIndicatorRule,
getNewRule,
getNewThresholdRule,
totalNumberOfPrebuiltRules,
getMachineLearningRule,
getNewTermsRule,
} from '../../objects/rule';
import { getIndicatorMatchTimelineTemplate } from '../../objects/timeline';
import { esArchiverResetKibana } from '../../tasks/es_archiver';
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 prePopulatedIndexPatterns = ['index-1-*', 'index-2-*'];
const prePopulatedTags = ['test-default-tag-1', 'test-default-tag-2'];
const expectedNumberOfCustomRulesToBeEdited = 6;
const expectedNumberOfMachineLearningRulesToBeEdited = 1;
const timelineTemplate = getIndicatorMatchTimelineTemplate();
/**
* total number of custom rules that are not Machine learning
*/
const expectedNumberOfNotMLRules =
expectedNumberOfCustomRulesToBeEdited - expectedNumberOfMachineLearningRulesToBeEdited;
const numberOfRulesPerPage = 5;
const indexDataSource = { index: prePopulatedIndexPatterns, type: 'indexPatterns' } as const;
const defaultRuleData = {
dataSource: indexDataSource,
tags: prePopulatedTags,
timeline: timelineTemplate,
};
describe('Detection rules, bulk edit', () => {
before(() => {
cleanKibana();
@ -90,180 +130,358 @@ describe('Detection rules, bulk edit', () => {
{
...getNewRule(),
name: RULE_NAME,
dataSource: { index: DEFAULT_INDEX_PATTERNS, type: 'indexPatterns' },
...defaultRuleData,
},
'1'
);
createCustomRule(getExistingRule(), '2');
createCustomRule(getNewOverrideRule(), '3');
createCustomRule(getNewThresholdRule(), '4');
createCustomRule({ ...getNewRule(), name: 'rule # 5' }, '5');
createCustomRule({ ...getNewRule(), name: 'rule # 6' }, '6');
createEventCorrelationRule({ ...getEqlRule(), ...defaultRuleData }, '2');
createMachineLearningRule({ ...getMachineLearningRule(), ...defaultRuleData });
createCustomIndicatorRule({ ...getNewThreatIndicatorRule(), ...defaultRuleData }, '4');
createThresholdRule({ ...getNewThresholdRule(), ...defaultRuleData }, '5');
createNewTermsRule({ ...getNewTermsRule(), ...defaultRuleData }, '6');
visitWithoutDateRange(SECURITY_DETECTIONS_RULES_URL);
waitForRulesTableToBeLoaded();
});
it('should show warning modal windows when some of the selected rules cannot be edited', () => {
createMachineLearningRule(getMachineLearningRule(), '7');
describe('Prerequisites', () => {
it('No rules selected', () => {
openBulkActionsMenu();
loadPrebuiltDetectionRulesFromHeaderBtn();
// when no rule selected all bulk edit options should be disabled
cy.get(TAGS_RULE_BULK_MENU_ITEM).should('be.disabled');
cy.get(INDEX_PATTERNS_RULE_BULK_MENU_ITEM).should('be.disabled');
cy.get(APPLY_TIMELINE_RULE_BULK_MENU_ITEM).should('be.disabled');
});
// select few Elastic rules, check if we can't proceed further, as ELastic rules are not editable
// filter rules, only Elastic rule to show
switchToElasticRules();
it('Only prebuilt rules selected', () => {
const expectedNumberOfSelectedRules = 10;
// check modal window for few selected rules
selectNumberOfRules(numberOfRulesPerPage);
clickAddIndexPatternsMenuItem();
checkElasticRulesCannotBeModified(numberOfRulesPerPage);
cy.get(MODAL_CONFIRMATION_BTN).click();
loadPrebuiltDetectionRulesFromHeaderBtn();
// Select all rules(Elastic rules and custom)
cy.get(ELASTIC_RULES_BTN).click();
selectAllRules();
clickAddIndexPatternsMenuItem();
waitForMixedRulesBulkEditModal(expectedNumberOfCustomRulesToBeEdited);
// select Elastic(prebuilt) rules, check if we can't proceed further, as Elastic rules are not editable
switchToElasticRules();
selectNumberOfRules(expectedNumberOfSelectedRules);
clickApplyTimelineTemplatesMenuItem();
// check rules that cannot be edited for index patterns: immutable and ML
checkElasticRulesCannotBeModified(totalNumberOfPrebuiltRules);
checkMachineLearningRulesCannotBeModified(expectedNumberOfMachineLearningRulesToBeEdited);
// check modal window for Elastic rule that can't be edited
checkPrebuiltRulesCannotBeModified(expectedNumberOfSelectedRules);
// proceed with custom rule editing
cy.get(MODAL_CONFIRMATION_BTN)
.should('have.text', `Edit ${expectedNumberOfCustomRulesToBeEdited} custom rules`)
.click();
// the confirm button closes modal
cy.get(MODAL_CONFIRMATION_BTN).should('have.text', 'Close').click();
cy.get(MODAL_CONFIRMATION_BODY).should('not.exist');
});
typeIndexPatterns([CUSTOM_INDEX_PATTERN_1]);
confirmBulkEditForm();
it('Prebuilt and custom rules selected: user proceeds with custom rules editing', () => {
loadPrebuiltDetectionRulesFromHeaderBtn();
// check if rule has been updated
cy.get(CUSTOM_RULES_BTN).click();
cy.get(RULES_TABLE_REFRESH_INDICATOR).should('exist');
cy.get(RULES_TABLE_REFRESH_INDICATOR).should('not.exist');
goToTheRuleDetailsOf(RULE_NAME);
hasIndexPatterns([...DEFAULT_INDEX_PATTERNS, CUSTOM_INDEX_PATTERN_1].join(''));
// modal window should show how many rules can be edit, how many not
selectAllRules();
clickAddTagsMenuItem();
waitForMixedRulesBulkEditModal(expectedNumberOfCustomRulesToBeEdited);
checkPrebuiltRulesCannotBeModified(totalNumberOfPrebuiltRules);
// user can proceed with custom rule editing
cy.get(MODAL_CONFIRMATION_BTN)
.should('have.text', `Edit ${expectedNumberOfCustomRulesToBeEdited} custom rules`)
.click();
// action should finish
typeTags(['test-tag']);
submitBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited });
});
it('Prebuilt and custom rules selected: user cancels action', () => {
loadPrebuiltDetectionRulesFromHeaderBtn();
// modal window should show how many rules can be edit, how many not
selectAllRules();
clickAddTagsMenuItem();
waitForMixedRulesBulkEditModal(expectedNumberOfCustomRulesToBeEdited);
checkPrebuiltRulesCannotBeModified(totalNumberOfPrebuiltRules);
// user cancels action and modal disappears
cancelConfirmationModal();
});
it('should not lose rules selection after edit action', () => {
const rulesCount = 4;
// Switch to 5 rules per page, to have few pages in pagination(ideal way to test auto refresh and selection of few items)
changeRowsPerPageTo(numberOfRulesPerPage);
selectNumberOfRules(rulesCount);
// open add tags form and add 2 new tags
openBulkEditAddTagsForm();
typeTags(prePopulatedTags);
submitBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount });
testMultipleSelectedRulesLabel(rulesCount);
// check if first four(rulesCount) rules still selected and tags are updated
for (let i = 0; i < rulesCount; i += 1) {
cy.get(RULE_CHECKBOX).eq(i).should('be.checked');
cy.get(RULES_TAGS_POPOVER_BTN)
.eq(i)
.each(($el) => {
testTagsBadge($el, prePopulatedTags);
});
}
});
});
it('should add/delete/overwrite index patterns in rules', () => {
cy.log('Adds index patterns');
// Switch to 5(numberOfRulesPerPage) 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(numberOfRulesPerPage);
selectAllRules();
describe('Tags actions', () => {
it('Display list of tags in tags select', () => {
selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited);
openBulkEditAddIndexPatternsForm();
typeIndexPatterns([CUSTOM_INDEX_PATTERN_1]);
confirmBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited });
openBulkEditAddTagsForm();
openTagsSelect();
// 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: expectedNumberOfCustomRulesToBeEdited });
// 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 ${expectedNumberOfCustomRulesToBeEdited} selected rules, press Save to apply changes.`
);
typeIndexPatterns(OVERWRITE_INDEX_PATTERNS);
confirmBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited });
// 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(numberOfRulesPerPage) 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(numberOfRulesPerPage);
selectAllRules();
// open add tags form and add 2 new tags
openBulkEditAddTagsForm();
typeTags(TAGS);
confirmBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited });
// 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: expectedNumberOfCustomRulesToBeEdited });
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 ${expectedNumberOfCustomRulesToBeEdited} selected rules, press Save to apply changes.`
);
typeTags(['overwrite-tag']);
confirmBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited });
testAllTagsBadges(['overwrite-tag']);
});
it('should not lose rules selection after edit action', () => {
const rulesCount = 4;
// Switch to 5 rules per page, to have few pages in pagination(ideal way to test auto refresh and selection of few items)
changeRowsPerPageTo(numberOfRulesPerPage);
selectNumberOfRules(rulesCount);
// open add tags form and add 2 new tags
openBulkEditAddTagsForm();
typeTags(TAGS);
confirmBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount });
testMultipleSelectedRulesLabel(rulesCount);
// check if first four(rulesCount) rules still selected and tags are updated
for (let i = 0; i < rulesCount; i += 1) {
cy.get(RULE_CHECKBOX).eq(i).should('be.checked');
cy.get(RULES_TAGS_POPOVER_BTN)
.eq(i)
.each(($el) => {
testTagsBadge($el, TAGS);
cy.get(EUI_FILTER_SELECT_ITEM)
.should('have.length', prePopulatedTags.length)
.each(($el, index) => {
cy.wrap($el).should('have.text', prePopulatedTags[index]);
});
}
});
it('Add tags to custom rules', () => {
const tagsToBeAdded = ['tag-to-add-1', 'tag-to-add-2'];
const resultingTags = [...prePopulatedTags, ...tagsToBeAdded];
// check if only pre-populated tags exist in the tags filter
checkTagsInTagsFilter(prePopulatedTags);
cy.get(EUI_FILTER_SELECT_ITEM)
.should('have.length', prePopulatedTags.length)
.each(($el, index) => {
cy.wrap($el).should('have.text', prePopulatedTags[index]);
});
selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited);
// open add tags form and add 2 new tags
openBulkEditAddTagsForm();
typeTags(tagsToBeAdded);
submitBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited });
// check if all rules have been updated with new tags
testAllTagsBadges(resultingTags);
// check that new tags were added to tags filter
// tags in tags filter sorted alphabetically
const resultingTagsInFilter = [...resultingTags].sort();
checkTagsInTagsFilter(resultingTagsInFilter);
});
it('Overwrite tags in custom rules', () => {
const tagsToOverwrite = ['overwrite-tag-1'];
// check if only pre-populated tags exist in the tags filter
checkTagsInTagsFilter(prePopulatedTags);
selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited);
// open add tags form, check overwrite tags and warning message, type tags
openBulkEditAddTagsForm();
checkOverwriteTagsCheckbox();
cy.get(RULES_BULK_EDIT_TAGS_WARNING).should(
'have.text',
`Youre about to overwrite tags for ${expectedNumberOfCustomRulesToBeEdited} selected rules, press Save to apply changes.`
);
typeTags(tagsToOverwrite);
submitBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited });
// check if all rules have been updated with new tags
testAllTagsBadges(tagsToOverwrite);
// check that only new tags are in the tag filter
checkTagsInTagsFilter(tagsToOverwrite);
});
it('Delete tags from custom rules', () => {
const tagsToDelete = prePopulatedTags.slice(0, 1);
const resultingTags = prePopulatedTags.slice(1);
// check if only pre-populated tags exist in the tags filter
checkTagsInTagsFilter(prePopulatedTags);
selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited);
// open add tags form, check overwrite tags, type tags
openBulkEditDeleteTagsForm();
typeTags(tagsToDelete);
submitBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited });
// check tags has been removed from all rules
testAllTagsBadges(resultingTags);
// check that tags were removed from the tag filter
checkTagsInTagsFilter(resultingTags);
});
});
describe('Index patterns', () => {
it('Index pattern action applied to custom rules, including machine learning: user proceeds with edit of custom non machine learning rule', () => {
const indexPattersToBeAdded = ['index-to-add-1-*', 'index-to-add-2-*'];
const resultingIndexPatterns = [...prePopulatedIndexPatterns, ...indexPattersToBeAdded];
selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited);
clickAddIndexPatternsMenuItem();
// confirm editing custom rules, that are not Machine Learning
checkMachineLearningRulesCannotBeModified(expectedNumberOfMachineLearningRulesToBeEdited);
cy.get(MODAL_CONFIRMATION_BTN).click();
typeIndexPatterns(indexPattersToBeAdded);
submitBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfNotMLRules });
// check if rule has been updated
goToTheRuleDetailsOf(RULE_NAME);
hasIndexPatterns(resultingIndexPatterns.join(''));
});
it('Index pattern action applied to custom rules, including machine learning: user cancels action', () => {
selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited);
clickAddIndexPatternsMenuItem();
// confirm editing custom rules, that are not Machine Learning
checkMachineLearningRulesCannotBeModified(expectedNumberOfMachineLearningRulesToBeEdited);
// user cancels action and modal disappears
cancelConfirmationModal();
});
it('Add index patterns to custom rules', () => {
const indexPattersToBeAdded = ['index-to-add-1-*', 'index-to-add-2-*'];
const resultingIndexPatterns = [...prePopulatedIndexPatterns, ...indexPattersToBeAdded];
// select only rules that are not ML
selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited);
unselectRuleByName(getMachineLearningRule().name);
openBulkEditAddIndexPatternsForm();
typeIndexPatterns(indexPattersToBeAdded);
submitBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfNotMLRules });
// check if rule has been updated
goToTheRuleDetailsOf(RULE_NAME);
hasIndexPatterns(resultingIndexPatterns.join(''));
});
it('Overwrite index patterns in custom rules', () => {
const indexPattersToWrite = ['index-to-write-1-*', 'index-to-write-2-*'];
// select only rules that are not ML
selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited);
unselectRuleByName(getMachineLearningRule().name);
openBulkEditAddIndexPatternsForm();
// check overwrite index patterns checkbox, ensure warning message is displayed and type index patterns
checkOverwriteIndexPatternsCheckbox();
cy.get(RULES_BULK_EDIT_INDEX_PATTERNS_WARNING).should(
'have.text',
`Youre about to overwrite index patterns for ${expectedNumberOfNotMLRules} selected rules, press Save to apply changes.`
);
typeIndexPatterns(indexPattersToWrite);
submitBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfNotMLRules });
// check if rule has been updated
goToTheRuleDetailsOf(RULE_NAME);
hasIndexPatterns(indexPattersToWrite.join(''));
});
it('Delete index patterns from custom rules', () => {
const indexPatternsToDelete = prePopulatedIndexPatterns.slice(0, 1);
const resultingIndexPatterns = prePopulatedIndexPatterns.slice(1);
// select only not ML rules
selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited);
unselectRuleByName(getMachineLearningRule().name);
openBulkEditDeleteIndexPatternsForm();
typeIndexPatterns(indexPatternsToDelete);
submitBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfNotMLRules });
// check if rule has been updated
goToTheRuleDetailsOf(RULE_NAME);
hasIndexPatterns(resultingIndexPatterns.join(''));
});
it('Delete all index patterns from custom rules', () => {
// select only rules that are not ML
selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited);
unselectRuleByName(getMachineLearningRule().name);
openBulkEditDeleteIndexPatternsForm();
typeIndexPatterns(prePopulatedIndexPatterns);
submitBulkEditForm();
// error toast should be displayed that that rules edit failed
cy.contains(TOASTER_BODY, `${expectedNumberOfNotMLRules} rules failed to update.`);
// on error toast button click display error that index patterns can't be empty
clickErrorToastBtn();
cy.contains(MODAL_ERROR_BODY, "Index patterns can't be empty");
});
});
describe('Timeline templates', () => {
beforeEach(() => {
loadPrepackagedTimelineTemplates();
});
it('Apply timeline template to custom rules', () => {
const timelineTemplateName = 'Generic Endpoint Timeline';
selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited);
// open Timeline template form, check warning, select timeline template
clickApplyTimelineTemplatesMenuItem();
cy.get(RULES_BULK_EDIT_TIMELINE_TEMPLATES_WARNING).contains(
`You're about to apply changes to ${expectedNumberOfCustomRulesToBeEdited} selected rules. If you previously applied Timeline templates to these rules, they will be overwritten or (if you select 'None') reset to none.`
);
selectTimelineTemplate(timelineTemplateName);
submitBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited });
// check if timeline template has been updated to selected one
goToTheRuleDetailsOf(RULE_NAME);
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', timelineTemplateName);
});
it('Reset timeline template to None for custom rules', () => {
const noneTimelineTemplate = 'None';
selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited);
// open Timeline template form, submit form without picking timeline template as None is selected by default
clickApplyTimelineTemplatesMenuItem();
submitBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited });
// check if timeline template has been updated to selected one, by opening rule that have had timeline prior to editing
goToTheRuleDetailsOf(RULE_NAME);
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', noneTimelineTemplate);
});
});
});

View file

@ -499,6 +499,9 @@ export const expectedExportedRule = (ruleResponse: Cypress.Response<RulesSchema>
risk_score: riskScore,
severity,
query,
tags,
timeline_id: timelineId,
timeline_title: timelineTitle,
} = ruleResponse.body;
// NOTE: Order of the properties in this object matters for the tests to work.
@ -509,7 +512,7 @@ export const expectedExportedRule = (ruleResponse: Cypress.Response<RulesSchema>
created_at: createdAt,
created_by: 'elastic',
name,
tags: [],
tags,
interval: '100m',
enabled: false,
description,
@ -538,6 +541,8 @@ export const expectedExportedRule = (ruleResponse: Cypress.Response<RulesSchema>
query,
throttle: 'no_actions',
actions: [],
timeline_id: timelineId,
timeline_title: timelineTitle,
};
// NOTE: Order of the properties in this object matters for the tests to work.

View file

@ -105,6 +105,10 @@ export const MODAL_CONFIRMATION_TITLE = '[data-test-subj="confirmModalTitleText"
export const MODAL_CONFIRMATION_BODY = '[data-test-subj="confirmModalBodyText"]';
export const MODAL_ERROR_BODY = '[data-test-subj="errorModalBody"]';
export const MODAL_CONFIRMATION_CANCEL_BTN = '[data-test-subj="confirmModalCancelButton"]';
export const RULE_DETAILS_DELETE_BTN = '[data-test-subj="rules-details-delete-rule"]';
export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count"]';
@ -121,6 +125,8 @@ export const TOASTER = '[data-test-subj="euiToastHeader"]';
export const TOASTER_BODY = '[data-test-subj="globalToastList"] [data-test-subj="euiToastBody"]';
export const TOASTER_ERROR_BTN = '[data-test-subj="errorToastBtn"]';
export const RULE_IMPORT_OVERWRITE_CHECKBOX = '[id="import-data-modal-checkbox-label"]';
export const RULE_IMPORT_OVERWRITE_EXCEPTIONS_CHECKBOX =

View file

@ -0,0 +1,12 @@
/*
* 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 TIMELINE_SEARCHBOX = '[data-test-subj="timeline-super-select-search-box"]';
export const EUI_FILTER_SELECT_ITEM = '.euiFilterSelectItem';
export const EUI_CHECKBOX = '.euiCheckbox__input';

View file

@ -21,6 +21,4 @@ export const TAGS_INPUT = '[data-test-subj="caseTags"] [data-test-subj="comboBox
export const TIMELINE = '[data-test-subj="timeline"]';
export const TIMELINE_SEARCHBOX = '[data-test-subj="timeline-super-select-search-box"]';
export const TITLE_INPUT = '[data-test-subj="caseTitle"] [data-test-subj="input"]';

View file

@ -223,8 +223,6 @@ export const TAGS_INPUT =
export const TAGS_CLEAR_BUTTON =
'[data-test-subj="detectionEngineStepAboutRuleTags"] [data-test-subj="comboBoxClearButton"]';
export const EUI_FILTER_SELECT_ITEM = '.euiFilterSelectItem';
export const THRESHOLD_INPUT_AREA = '[data-test-subj="thresholdInput"]';
export const THRESHOLD_TYPE = '[data-test-subj="thresholdRuleType"]';

View file

@ -13,6 +13,8 @@ export const ADD_INDEX_PATTERNS_RULE_BULK_MENU_ITEM =
export const DELETE_INDEX_PATTERNS_RULE_BULK_MENU_ITEM =
'[data-test-subj="deleteIndexPatternsBulkEditRule"]';
export const APPLY_TIMELINE_RULE_BULK_MENU_ITEM = '[data-test-subj="applyTimelineTemplateBulk"]';
export const TAGS_RULE_BULK_MENU_ITEM = '[data-test-subj="tagsBulkEditRule"]';
export const ADD_TAGS_RULE_BULK_MENU_ITEM = '[data-test-subj="addTagsBulkEditRule"]';
@ -37,3 +39,9 @@ export const RULES_BULK_EDIT_INDEX_PATTERNS_WARNING =
'[data-test-subj="bulkEditRulesIndexPatternsWarning"]';
export const RULES_BULK_EDIT_TAGS_WARNING = '[data-test-subj="bulkEditRulesTagsWarning"]';
export const RULES_BULK_EDIT_TIMELINE_TEMPLATES_SELECTOR =
'[data-test-subj="bulkEditRulesTimelineTemplateSelector"]';
export const RULES_BULK_EDIT_TIMELINE_TEMPLATES_WARNING =
'[data-test-subj="bulkEditRulesTimelineTemplateWarning"]';

View file

@ -53,7 +53,11 @@ import {
REFRESH_SETTINGS_SWITCH,
ELASTIC_RULES_BTN,
BULK_EXPORT_ACTION_BTN,
TOASTER_ERROR_BTN,
MODAL_CONFIRMATION_CANCEL_BTN,
MODAL_CONFIRMATION_BODY,
} from '../screens/alerts_detection_rules';
import { EUI_CHECKBOX } from '../screens/common/controls';
import { ALL_ACTIONS } from '../screens/rule_details';
import { LOADING_INDICATOR } from '../screens/security_header';
@ -171,6 +175,9 @@ export const loadPrebuiltDetectionRules = () => {
.should('be.disabled');
};
/**
* load prebuilt rules by clicking button on page header
*/
export const loadPrebuiltDetectionRulesFromHeaderBtn = () => {
cy.get(LOAD_PREBUILT_RULES_ON_PAGE_HEADER_BTN)
.pipe(($el) => $el.trigger('click'))
@ -201,6 +208,14 @@ export const selectNumberOfRules = (numberOfRules: number) => {
}
};
export const unselectRuleByName = (ruleName: string) => {
cy.contains(RULE_NAME, ruleName)
.parents(RULES_ROW)
.find(EUI_CHECKBOX)
.click()
.should('not.be.checked');
};
/**
* Unselects a passed number of rules. To use together with selectNumberOfRules
* as this utility will expect and check the passed number of rules
@ -363,3 +378,12 @@ export const bulkExportRules = () => {
cy.get(BULK_ACTIONS_BTN).click();
cy.get(BULK_EXPORT_ACTION_BTN).click();
};
export const cancelConfirmationModal = () => {
cy.get(MODAL_CONFIRMATION_CANCEL_BTN).click();
cy.get(MODAL_CONFIRMATION_BODY).should('not.exist');
};
export const clickErrorToastBtn = () => {
cy.get(TOASTER_ERROR_BTN).click();
};

View file

@ -5,7 +5,13 @@
* 2.0.
*/
import type { CustomRule, ThreatIndicatorRule, MachineLearningRule } from '../../objects/rule';
import type {
CustomRule,
ThreatIndicatorRule,
MachineLearningRule,
ThresholdRule,
NewTermsRule,
} from '../../objects/rule';
export const createMachineLearningRule = (rule: MachineLearningRule, ruleId = 'ml_rule_testing') =>
cy.request({
@ -23,6 +29,7 @@ export const createMachineLearningRule = (rule: MachineLearningRule, ruleId = 'm
enabled: false,
machine_learning_job_id: rule.machineLearningJobs,
anomaly_threshold: rule.anomalyScoreThreshold,
tags: rule.tags,
},
headers: { 'kbn-xsrf': 'cypress-creds' },
failOnStatusCode: false,
@ -50,6 +57,13 @@ export const createCustomRule = (
language: 'kuery',
enabled: false,
exceptions_list: rule.exceptionLists ?? [],
tags: rule.tags,
...(rule.timeline.id ?? rule.timeline.templateTimelineId
? {
timeline_id: rule.timeline.id ?? rule.timeline.templateTimelineId,
timeline_title: rule.timeline.title,
}
: {}),
},
headers: { 'kbn-xsrf': 'cypress-creds' },
failOnStatusCode: false,
@ -73,6 +87,62 @@ export const createEventCorrelationRule = (rule: CustomRule, ruleId = 'rule_test
query: rule.customQuery,
language: 'eql',
enabled: true,
tags: rule.tags,
},
headers: { 'kbn-xsrf': 'cypress-creds' },
});
}
};
export const createThresholdRule = (rule: ThresholdRule, ruleId = 'rule_testing') => {
if (rule.dataSource.type === 'indexPatterns') {
cy.request({
method: 'POST',
url: 'api/detection_engine/rules',
body: {
rule_id: ruleId,
risk_score: parseInt(rule.riskScore, 10),
description: rule.description,
interval: `${rule.runsEvery.interval}${rule.runsEvery.type}`,
from: `now-${rule.lookBack.interval}${rule.lookBack.type}`,
name: rule.name,
severity: rule.severity.toLocaleLowerCase(),
type: 'threshold',
index: rule.dataSource.index,
query: rule.customQuery,
threshold: {
field: [rule.thresholdField],
value: parseInt(rule.threshold, 10),
cardinality: [],
},
enabled: true,
tags: rule.tags,
},
headers: { 'kbn-xsrf': 'cypress-creds' },
});
}
};
export const createNewTermsRule = (rule: NewTermsRule, ruleId = 'rule_testing') => {
if (rule.dataSource.type === 'indexPatterns') {
cy.request({
method: 'POST',
url: 'api/detection_engine/rules',
body: {
rule_id: ruleId,
risk_score: parseInt(rule.riskScore, 10),
description: rule.description,
interval: `${rule.runsEvery.interval}${rule.runsEvery.type}`,
from: `now-${rule.lookBack.interval}${rule.lookBack.type}`,
name: rule.name,
severity: rule.severity.toLocaleLowerCase(),
type: 'new_terms',
index: rule.dataSource.index,
query: rule.customQuery,
new_terms_fields: rule.newTermsFields,
history_window_start: `now-${rule.historyWindowSize.interval}${rule.historyWindowSize.type}`,
enabled: true,
tags: rule.tags,
},
headers: { 'kbn-xsrf': 'cypress-creds' },
});
@ -117,6 +187,7 @@ export const createCustomIndicatorRule = (rule: ThreatIndicatorRule, ruleId = 'r
query: rule.customQuery || '*:*',
language: 'kuery',
enabled: true,
tags: rule.tags,
},
headers: { 'kbn-xsrf': 'cypress-creds' },
failOnStatusCode: false,

View file

@ -14,6 +14,7 @@ import type {
} from '../objects/case';
import { ALL_CASES_OPEN_CASES_COUNT, ALL_CASES_OPEN_FILTER } from '../screens/all_cases';
import { TIMELINE_SEARCHBOX } from '../screens/common/controls';
import {
BACK_TO_CASES_BTN,
DESCRIPTION_INPUT,
@ -21,7 +22,6 @@ import {
INSERT_TIMELINE_BTN,
LOADING_SPINNER,
TAGS_INPUT,
TIMELINE_SEARCHBOX,
TITLE_INPUT,
} from '../screens/create_new_case';
import {

View file

@ -83,7 +83,6 @@ import {
THREAT_MATCH_INDICATOR_INDICATOR_INDEX,
THREAT_MATCH_OR_BUTTON,
THREAT_MATCH_QUERY_INPUT,
EUI_FILTER_SELECT_ITEM,
THRESHOLD_INPUT_AREA,
THRESHOLD_TYPE,
CONNECTOR_NAME_INPUT,
@ -105,6 +104,7 @@ import { TOAST_ERROR } from '../screens/shared';
import { SERVER_SIDE_EVENT_COUNT } from '../screens/timeline';
import { TIMELINE } from '../screens/timelines';
import { refreshPage } from './security_header';
import { EUI_FILTER_SELECT_ITEM } from '../screens/common/controls';
export const createAndEnableRule = () => {
cy.get(SCHEDULE_CONTINUE_BUTTON).click({ force: true });

View file

@ -5,12 +5,15 @@
* 2.0.
*/
import { TIMELINE_SEARCHBOX, EUI_FILTER_SELECT_ITEM } from '../screens/common/controls';
import {
BULK_ACTIONS_BTN,
BULK_ACTIONS_PROGRESS_BTN,
MODAL_CONFIRMATION_TITLE,
MODAL_CONFIRMATION_BODY,
TOASTER_BODY,
RULES_TAGS_FILTER_BTN,
} from '../screens/alerts_detection_rules';
import {
@ -24,14 +27,42 @@ import {
RULES_BULK_EDIT_INDEX_PATTERNS,
RULES_BULK_EDIT_TAGS,
RULES_BULK_EDIT_FORM_CONFIRM_BTN,
APPLY_TIMELINE_RULE_BULK_MENU_ITEM,
RULES_BULK_EDIT_OVERWRITE_TAGS_CHECKBOX,
RULES_BULK_EDIT_OVERWRITE_INDEX_PATTERNS_CHECKBOX,
RULES_BULK_EDIT_TIMELINE_TEMPLATES_SELECTOR,
} from '../screens/rules_bulk_edit';
export const clickAddIndexPatternsMenuItem = () => {
export const clickApplyTimelineTemplatesMenuItem = () => {
cy.get(BULK_ACTIONS_BTN).click();
cy.get(INDEX_PATTERNS_RULE_BULK_MENU_ITEM).click();
cy.get(APPLY_TIMELINE_RULE_BULK_MENU_ITEM).click().should('not.exist');
};
export const clickIndexPatternsMenuItem = () => {
cy.get(BULK_ACTIONS_BTN).click();
cy.get(INDEX_PATTERNS_RULE_BULK_MENU_ITEM).click().should('not.exist');
};
export const clickTagsMenuItem = () => {
cy.get(BULK_ACTIONS_BTN).click();
cy.get(TAGS_RULE_BULK_MENU_ITEM).click();
};
export const clickAddTagsMenuItem = () => {
clickTagsMenuItem();
cy.get(ADD_TAGS_RULE_BULK_MENU_ITEM).click();
};
export const clickAddIndexPatternsMenuItem = () => {
clickIndexPatternsMenuItem();
cy.get(ADD_INDEX_PATTERNS_RULE_BULK_MENU_ITEM).click();
};
export const clickDeleteIndexPatternsMenuItem = () => {
clickIndexPatternsMenuItem();
cy.get(DELETE_INDEX_PATTERNS_RULE_BULK_MENU_ITEM).click().should('not.exist');
};
export const openBulkEditAddIndexPatternsForm = () => {
clickAddIndexPatternsMenuItem();
@ -47,21 +78,22 @@ export const openBulkEditDeleteIndexPatternsForm = () => {
};
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();
clickAddTagsMenuItem();
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();
clickTagsMenuItem();
cy.get(DELETE_TAGS_RULE_BULK_MENU_ITEM).click();
cy.get(RULES_BULK_EDIT_FORM_TITLE).should('have.text', 'Delete tags');
};
export const openBulkActionsMenu = () => {
cy.get(BULK_ACTIONS_BTN).click();
};
export const typeIndexPatterns = (indices: string[]) => {
cy.get(RULES_BULK_EDIT_INDEX_PATTERNS).find('input').type(indices.join('{enter}'));
};
@ -70,14 +102,18 @@ 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 openTagsSelect = () => {
cy.get(RULES_BULK_EDIT_TAGS).find('input').click();
};
export const submitBulkEditForm = () => 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 checkElasticRulesCannotBeModified = (rulesCount: number) => {
export const checkPrebuiltRulesCannotBeModified = (rulesCount: number) => {
cy.get(MODAL_CONFIRMATION_BODY).contains(
`${rulesCount} prebuilt Elastic rules (editing prebuilt rules is not supported)`
);
@ -95,3 +131,38 @@ export const waitForMixedRulesBulkEditModal = (customRulesCount: number) => {
`This action can only be applied to ${customRulesCount} custom rules`
);
};
export const checkOverwriteTagsCheckbox = () => {
cy.get(RULES_BULK_EDIT_OVERWRITE_TAGS_CHECKBOX)
.should('have.text', "Overwrite all selected rules' tags")
.click()
.get('input')
.should('be.checked');
};
export const checkOverwriteIndexPatternsCheckbox = () => {
cy.get(RULES_BULK_EDIT_OVERWRITE_INDEX_PATTERNS_CHECKBOX)
.should('have.text', "Overwrite all selected rules' index patterns")
.click()
.get('input')
.should('be.checked');
};
export const selectTimelineTemplate = (timelineTitle: string) => {
cy.get(RULES_BULK_EDIT_TIMELINE_TEMPLATES_SELECTOR).click();
cy.get(TIMELINE_SEARCHBOX).type(`${timelineTitle}{enter}`).should('not.exist');
};
/**
* check if rule tags filter populated with a list of tags
* @param tags
*/
export const checkTagsInTagsFilter = (tags: string[]) => {
cy.get(RULES_TAGS_FILTER_BTN).contains(`Tags${tags.length}`).click();
cy.get(EUI_FILTER_SELECT_ITEM)
.should('have.length', tags.length)
.each(($el, index) => {
cy.wrap($el).should('have.text', tags[index]);
});
};

View file

@ -38,7 +38,7 @@ describe('deleteItemsFromArray', () => {
});
describe('ruleParamsModifier', () => {
const ruleParamsMock = { index: ['initial-index-*'], version: 1 } as RuleAlertType['params'];
const ruleParamsMock = { index: ['my-index-*'], version: 1 } as RuleAlertType['params'];
test('should increment version', () => {
const editedRuleParams = ruleParamsModifier(ruleParamsMock, [
@ -51,26 +51,161 @@ describe('ruleParamsModifier', () => {
});
describe('index_patterns', () => {
test('should add new index pattern to rule', () => {
const editedRuleParams = ruleParamsModifier(ruleParamsMock, [
{
type: BulkActionEditType.add_index_patterns,
value: ['my-index-*'],
},
]);
expect(editedRuleParams).toHaveProperty('index', ['initial-index-*', 'my-index-*']);
});
test('should remove index pattern from rule', () => {
const editedRuleParams = ruleParamsModifier(
{ index: ['initial-index-*', 'index-2-*'] } as RuleAlertType['params'],
describe('add_index_patterns action', () => {
test.each([
[
'3 existing patterns + 2 of them = 3 patterns',
{
type: BulkActionEditType.delete_index_patterns,
value: ['index-2-*'],
existingIndexPatterns: ['index-1-*', 'index-2-*', 'index-3-*'],
indexPatternsToAdd: ['index-2-*', 'index-3-*'],
resultingIndexPatterns: ['index-1-*', 'index-2-*', 'index-3-*'],
},
]
],
[
'3 existing patterns + 2 other patterns(none of them) = 5 patterns',
{
existingIndexPatterns: ['index-1-*', 'index-2-*', 'index-3-*'],
indexPatternsToAdd: ['index-4-*', 'index-5-*'],
resultingIndexPatterns: [
'index-1-*',
'index-2-*',
'index-3-*',
'index-4-*',
'index-5-*',
],
},
],
[
'3 existing patterns + 1 of them + 2 other patterns(none of them) = 5 patterns',
{
existingIndexPatterns: ['index-1-*', 'index-2-*', 'index-3-*'],
indexPatternsToAdd: ['index-3-*', 'index-4-*', 'index-5-*'],
resultingIndexPatterns: [
'index-1-*',
'index-2-*',
'index-3-*',
'index-4-*',
'index-5-*',
],
},
],
[
'3 existing patterns + 0 patterns = 3 patterns',
{
existingIndexPatterns: ['index-1-*', 'index-2-*', 'index-3-*'],
indexPatternsToAdd: [],
resultingIndexPatterns: ['index-1-*', 'index-2-*', 'index-3-*'],
},
],
])(
'should add index patterns to rule, case:"%s"',
(caseName, { existingIndexPatterns, indexPatternsToAdd, resultingIndexPatterns }) => {
const editedRuleParams = ruleParamsModifier(
{ ...ruleParamsMock, index: existingIndexPatterns } as RuleAlertType['params'],
[
{
type: BulkActionEditType.add_index_patterns,
value: indexPatternsToAdd,
},
]
);
expect(editedRuleParams).toHaveProperty('index', resultingIndexPatterns);
}
);
});
describe('delete_index_patterns action', () => {
test.each([
[
'3 existing patterns - 2 of them = 1 pattern',
{
existingIndexPatterns: ['index-1-*', 'index-2-*', 'index-3-*'],
indexPatternsToDelete: ['index-2-*', 'index-3-*'],
resultingIndexPatterns: ['index-1-*'],
},
],
[
'3 existing patterns - 2 other patterns(none of them) = 3 patterns',
{
existingIndexPatterns: ['index-1-*', 'index-2-*', 'index-3-*'],
indexPatternsToDelete: ['index-4-*', 'index-5-*'],
resultingIndexPatterns: ['index-1-*', 'index-2-*', 'index-3-*'],
},
],
[
'3 existing patterns - 1 of them - 2 other patterns(none of them) = 2 patterns',
{
existingIndexPatterns: ['index-1-*', 'index-2-*', 'index-3-*'],
indexPatternsToDelete: ['index-3-*', 'index-4-*', 'index-5-*'],
resultingIndexPatterns: ['index-1-*', 'index-2-*'],
},
],
[
'3 existing patterns - 0 patterns = 3 patterns',
{
existingIndexPatterns: ['index-1-*', 'index-2-*', 'index-3-*'],
indexPatternsToDelete: [],
resultingIndexPatterns: ['index-1-*', 'index-2-*', 'index-3-*'],
},
],
])(
'should delete index patterns from rule, case:"%s"',
(caseName, { existingIndexPatterns, indexPatternsToDelete, resultingIndexPatterns }) => {
const editedRuleParams = ruleParamsModifier(
{ ...ruleParamsMock, index: existingIndexPatterns } as RuleAlertType['params'],
[
{
type: BulkActionEditType.delete_index_patterns,
value: indexPatternsToDelete,
},
]
);
expect(editedRuleParams).toHaveProperty('index', resultingIndexPatterns);
}
);
});
describe('set_index_patterns action', () => {
test.each([
[
'3 existing patterns overwritten with 2 of them = 2 existing patterns',
{
existingIndexPatterns: ['index-1-*', 'index-2-*', 'index-3-*'],
indexPatternsToOverwrite: ['index-2-*', 'index-3-*'],
resultingIndexPatterns: ['index-2-*', 'index-3-*'],
},
],
[
'3 existing patterns overwritten with 2 other patterns = 2 other patterns',
{
existingIndexPatterns: ['index-1-*', 'index-2-*', 'index-3-*'],
indexPatternsToOverwrite: ['index-4-*', 'index-5-*'],
resultingIndexPatterns: ['index-4-*', 'index-5-*'],
},
],
[
'3 existing patterns overwritten with 1 of them + 2 other patterns = 1 existing pattern + 2 other patterns',
{
existingIndexPatterns: ['index-1-*', 'index-2-*', 'index-3-*'],
indexPatternsToOverwrite: ['index-3-*', 'index-4-*', 'index-5-*'],
resultingIndexPatterns: ['index-3-*', 'index-4-*', 'index-5-*'],
},
],
])(
'should overwrite index patterns in rule, case:"%s"',
(caseName, { existingIndexPatterns, indexPatternsToOverwrite, resultingIndexPatterns }) => {
const editedRuleParams = ruleParamsModifier(
{ ...ruleParamsMock, index: existingIndexPatterns } as RuleAlertType['params'],
[
{
type: BulkActionEditType.set_index_patterns,
value: indexPatternsToOverwrite,
},
]
);
expect(editedRuleParams).toHaveProperty('index', resultingIndexPatterns);
}
);
expect(editedRuleParams).toHaveProperty('index', ['initial-index-*']);
});
test('should return undefined index patterns on remove action if rule has dataViewId only', () => {
@ -88,16 +223,6 @@ describe('ruleParamsModifier', () => {
expect(editedRuleParams).toHaveProperty('dataViewId', testDataViewId);
});
test('should rewrite index pattern in rule', () => {
const editedRuleParams = ruleParamsModifier(ruleParamsMock, [
{
type: BulkActionEditType.set_index_patterns,
value: ['index'],
},
]);
expect(editedRuleParams).toHaveProperty('index', ['index']);
});
test('should set dataViewId to undefined if overwrite_data_views=true on set_index_patterns action', () => {
const editedRuleParams = ruleParamsModifier(
{ dataViewId: 'test-data-view', index: ['test-*'] } as RuleAlertType['params'],

View file

@ -362,73 +362,410 @@ export default ({ getService }: FtrProviderContext): void => {
});
describe('edit action', () => {
it('should set, add and delete tags in rules', async () => {
const ruleId = 'ruleId';
const tags = ['tag1', 'tag2'];
await createRule(supertest, log, getSimpleRule(ruleId));
describe('tags actions', () => {
const overwriteTagsCases = [
{
caseName: '3 existing tags overwritten with 2 of them = 2 existing tags',
existingTags: ['tag1', 'tag2', 'tag3'],
tagsToOverwrite: ['tag1', 'tag2'],
resultingTags: ['tag1', 'tag2'],
},
{
caseName: '3 existing tags overwritten with 2 other tags = 2 other tags',
existingTags: ['tag1', 'tag2', 'tag3'],
tagsToOverwrite: ['new-tag1', 'new-tag2'],
resultingTags: ['new-tag1', 'new-tag2'],
},
{
caseName:
'3 existing tags overwritten with 1 of them + 2 other tags = 1 existing tag + 2 other tags',
existingTags: ['tag1', 'tag2', 'tag3'],
tagsToOverwrite: ['tag1', 'new-tag1', 'new-tag2'],
resultingTags: ['tag1', 'new-tag1', 'new-tag2'],
},
{
caseName: '0 existing tags overwritten with 2 tags = 2 tags',
existingTags: [],
tagsToOverwrite: ['new-tag1', 'new-tag2'],
resultingTags: ['new-tag1', 'new-tag2'],
},
{
caseName: '3 existing tags overwritten with 0 tags = 0 tags',
existingTags: ['tag1', 'tag2', 'tag3'],
tagsToOverwrite: [],
resultingTags: [],
},
];
const { body: setTagsBody } = await postBulkAction()
.send({
query: '',
action: BulkAction.edit,
[BulkAction.edit]: [
overwriteTagsCases.forEach(({ caseName, existingTags, tagsToOverwrite, resultingTags }) => {
it(`should set tags in rules, case: "${caseName}"`, async () => {
const ruleId = 'ruleId';
await createRule(supertest, log, { ...getSimpleRule(ruleId), tags: existingTags });
const { body: bulkEditResponse } = await postBulkAction()
.send({
query: '',
action: BulkAction.edit,
[BulkAction.edit]: [
{
type: BulkActionEditType.set_tags,
value: tagsToOverwrite,
},
],
})
.expect(200);
expect(bulkEditResponse.attributes.summary).to.eql({
failed: 0,
succeeded: 1,
total: 1,
});
// Check that the updated rule is returned with the response
expect(bulkEditResponse.attributes.results.updated[0].tags).to.eql(resultingTags);
// Check that the updates have been persisted
const { body: updatedRule } = await fetchRule(ruleId).expect(200);
expect(updatedRule.tags).to.eql(resultingTags);
});
});
const deleteTagsCases = [
{
caseName: '3 existing tags - 2 of them = 1 tag',
existingTags: ['tag1', 'tag2', 'tag3'],
tagsToDelete: ['tag1', 'tag2'],
resultingTags: ['tag3'],
},
{
caseName: '3 existing tags - 2 other tags(none of them) = 3 tags',
existingTags: ['tag1', 'tag2', 'tag3'],
tagsToDelete: ['tag4', 'tag5'],
resultingTags: ['tag1', 'tag2', 'tag3'],
},
{
caseName: '3 existing tags - 1 of them - 2 other tags(none of them) = 2 tags',
existingTags: ['tag1', 'tag2', 'tag3'],
tagsToDelete: ['tag3', 'tag4', 'tag5'],
resultingTags: ['tag1', 'tag2'],
},
{
caseName: '3 existing tags - 0 tags = 3 tags',
existingTags: ['tag1', 'tag2', 'tag3'],
tagsToDelete: [],
resultingTags: ['tag1', 'tag2', 'tag3'],
},
{
caseName: '0 existing tags - 2 tags = 0 tags',
existingTags: [],
tagsToDelete: ['tag4', 'tag5'],
resultingTags: [],
},
{
caseName: '3 existing tags - 3 of them = 0 tags',
existingTags: ['tag1', 'tag2', 'tag3'],
tagsToDelete: ['tag1', 'tag2', 'tag3'],
resultingTags: [],
},
];
deleteTagsCases.forEach(({ caseName, existingTags, tagsToDelete, resultingTags }) => {
it(`should delete tags in rules, case: "${caseName}"`, async () => {
const ruleId = 'ruleId';
await createRule(supertest, log, { ...getSimpleRule(ruleId), tags: existingTags });
const { body: bulkEditResponse } = await postBulkAction()
.send({
query: '',
action: BulkAction.edit,
[BulkAction.edit]: [
{
type: BulkActionEditType.delete_tags,
value: tagsToDelete,
},
],
})
.expect(200);
expect(bulkEditResponse.attributes.summary).to.eql({
failed: 0,
succeeded: 1,
total: 1,
});
// Check that the updated rule is returned with the response
expect(bulkEditResponse.attributes.results.updated[0].tags).to.eql(resultingTags);
// Check that the updates have been persisted
const { body: updatedRule } = await fetchRule(ruleId).expect(200);
expect(updatedRule.tags).to.eql(resultingTags);
});
});
const addTagsCases = [
{
caseName: '3 existing tags + 2 of them = 3 tags',
existingTags: ['tag1', 'tag2', 'tag3'],
addedTags: ['tag1', 'tag2'],
resultingTags: ['tag1', 'tag2', 'tag3'],
},
{
caseName: '3 existing tags + 2 other tags(none of them) = 5 tags',
existingTags: ['tag1', 'tag2', 'tag3'],
addedTags: ['tag4', 'tag5'],
resultingTags: ['tag1', 'tag2', 'tag3', 'tag4', 'tag5'],
},
{
caseName: '3 existing tags + 1 of them + 2 other tags(none of them) = 5 tags',
existingTags: ['tag1', 'tag2', 'tag3'],
addedTags: ['tag4', 'tag5', 'tag1'],
resultingTags: ['tag1', 'tag2', 'tag3', 'tag4', 'tag5'],
},
{
caseName: '0 existing tags + 2 tags = 2 tags',
existingTags: [],
addedTags: ['tag4', 'tag5'],
resultingTags: ['tag4', 'tag5'],
},
{
caseName: '3 existing tags + 0 tags = 3 tags',
existingTags: ['tag1', 'tag2', 'tag3'],
addedTags: [],
resultingTags: ['tag1', 'tag2', 'tag3'],
},
];
addTagsCases.forEach(({ caseName, existingTags, addedTags, resultingTags }) => {
it(`should add tags to rules, case: "${caseName}"`, async () => {
const ruleId = 'ruleId';
await createRule(supertest, log, { ...getSimpleRule(ruleId), tags: existingTags });
const { body: bulkEditResponse } = await postBulkAction()
.send({
query: '',
action: BulkAction.edit,
[BulkAction.edit]: [
{
type: BulkActionEditType.add_tags,
value: addedTags,
},
],
})
.expect(200);
expect(bulkEditResponse.attributes.summary).to.eql({
failed: 0,
succeeded: 1,
total: 1,
});
// Check that the updated rule is returned with the response
expect(bulkEditResponse.attributes.results.updated[0].tags).to.eql(resultingTags);
// Check that the updates have been persisted
const { body: updatedRule } = await fetchRule(ruleId).expect(200);
expect(updatedRule.tags).to.eql(resultingTags);
});
});
});
describe('index patterns actions', () => {
it('should set index patterns in rules', async () => {
const ruleId = 'ruleId';
await createRule(supertest, log, getSimpleRule(ruleId));
const { body: bulkEditResponse } = await postBulkAction()
.send({
query: '',
action: BulkAction.edit,
[BulkAction.edit]: [
{
type: BulkActionEditType.set_index_patterns,
value: ['initial-index-*'],
},
],
})
.expect(200);
expect(bulkEditResponse.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 });
// Check that the updated rule is returned with the response
expect(bulkEditResponse.attributes.results.updated[0].index).to.eql(['initial-index-*']);
// Check that the updates have been persisted
const { body: updatedRule } = await fetchRule(ruleId).expect(200);
expect(updatedRule.index).to.eql(['initial-index-*']);
});
it('should add index patterns to rules', async () => {
const ruleId = 'ruleId';
const indexPatterns = ['index1-*', 'index2-*'];
const resultingIndexPatterns = ['index1-*', 'index2-*', 'index3-*'];
await createRule(supertest, log, { ...getSimpleRule(ruleId), index: indexPatterns });
const { body: bulkEditResponse } = await postBulkAction()
.send({
query: '',
action: BulkAction.edit,
[BulkAction.edit]: [
{
type: BulkActionEditType.add_index_patterns,
value: ['index3-*'],
},
],
})
.expect(200);
expect(bulkEditResponse.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 });
// Check that the updated rule is returned with the response
expect(bulkEditResponse.attributes.results.updated[0].index).to.eql(
resultingIndexPatterns
);
// Check that the updates have been persisted
const { body: updatedRule } = await fetchRule(ruleId).expect(200);
expect(updatedRule.index).to.eql(resultingIndexPatterns);
});
it('should delete index patterns from rules', async () => {
const ruleId = 'ruleId';
const indexPatterns = ['index1-*', 'index2-*'];
const resultingIndexPatterns = ['index1-*'];
await createRule(supertest, log, { ...getSimpleRule(ruleId), index: indexPatterns });
const { body: bulkEditResponse } = await postBulkAction()
.send({
query: '',
action: BulkAction.edit,
[BulkAction.edit]: [
{
type: BulkActionEditType.delete_index_patterns,
value: ['index2-*'],
},
],
})
.expect(200);
expect(bulkEditResponse.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 });
// Check that the updated rule is returned with the response
expect(bulkEditResponse.attributes.results.updated[0].index).to.eql(
resultingIndexPatterns
);
// Check that the updates have been persisted
const { body: updatedRule } = await fetchRule(ruleId).expect(200);
expect(updatedRule.index).to.eql(resultingIndexPatterns);
});
it('should return error if index patterns action is applied to machine learning rule', async () => {
const mlRule = await createRule(supertest, log, getSimpleMlRule());
const { body } = await postBulkAction()
.send({
ids: [mlRule.id],
action: BulkAction.edit,
[BulkAction.edit]: [
{
type: BulkActionEditType.add_index_patterns,
value: ['index-*'],
},
],
})
.expect(500);
expect(body.attributes.summary).to.eql({ failed: 1, succeeded: 0, total: 1 });
expect(body.attributes.errors[0]).to.eql({
message:
"Index patterns can't be added. Machine learning rule doesn't have index patterns property",
status_code: 500,
rules: [
{
type: BulkActionEditType.set_tags,
value: ['reset-tag'],
id: mlRule.id,
name: mlRule.name,
},
],
})
.expect(200);
});
});
expect(setTagsBody.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 });
it('should return error if all index patterns removed from a rule', async () => {
const rule = await createRule(supertest, log, {
...getSimpleRule(),
index: ['simple-index-*'],
});
// Check that the updated rule is returned with the response
expect(setTagsBody.attributes.results.updated[0].tags).to.eql(['reset-tag']);
const { body } = await postBulkAction()
.send({
ids: [rule.id],
action: BulkAction.edit,
[BulkAction.edit]: [
{
type: BulkActionEditType.delete_index_patterns,
value: ['simple-index-*'],
},
],
})
.expect(500);
// Check that the updates have been persisted
const { body: setTagsRule } = await fetchRule(ruleId).expect(200);
expect(setTagsRule.tags).to.eql(['reset-tag']);
const { body: addTagsBody } = await postBulkAction()
.send({
query: '',
action: BulkAction.edit,
[BulkAction.edit]: [
expect(body.attributes.summary).to.eql({ failed: 1, succeeded: 0, total: 1 });
expect(body.attributes.errors[0]).to.eql({
message: "Mutated params invalid: Index patterns can't be empty",
status_code: 500,
rules: [
{
type: BulkActionEditType.add_tags,
value: tags,
id: rule.id,
name: rule.name,
},
],
})
.expect(200);
});
});
expect(addTagsBody.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 });
it('should return error if index patterns set to empty list', async () => {
const ruleId = 'ruleId';
const rule = await createRule(supertest, log, {
...getSimpleRule(ruleId),
index: ['simple-index-*'],
});
// Check that the updated rule is returned with the response
expect(addTagsBody.attributes.results.updated[0].tags).to.eql(['reset-tag', ...tags]);
const { body } = await postBulkAction()
.send({
ids: [rule.id],
action: BulkAction.edit,
[BulkAction.edit]: [
{
type: BulkActionEditType.set_index_patterns,
value: [],
},
],
})
.expect(500);
// Check that the updates have been persisted
const { body: addedTagsRule } = await fetchRule(ruleId).expect(200);
expect(addedTagsRule.tags).to.eql(['reset-tag', ...tags]);
await postBulkAction()
.send({
query: '',
action: BulkAction.edit,
[BulkAction.edit]: [
expect(body.attributes.summary).to.eql({ failed: 1, succeeded: 0, total: 1 });
expect(body.attributes.errors[0]).to.eql({
message: "Mutated params invalid: Index patterns can't be empty",
status_code: 500,
rules: [
{
type: BulkActionEditType.delete_tags,
value: ['reset-tag', 'tag1'],
id: rule.id,
name: rule.name,
},
],
})
.expect(200);
});
const { body: deletedTagsRule } = await fetchRule(ruleId).expect(200);
// Check that the rule hasn't been updated
const { body: reFetchedRule } = await fetchRule(ruleId).expect(200);
expect(deletedTagsRule.tags).to.eql(['tag2']);
expect(reFetchedRule.index).to.eql(['simple-index-*']);
});
});
it('should migrate legacy actions on edit', async () => {
@ -490,79 +827,7 @@ export default ({ getService }: FtrProviderContext): void => {
]);
});
it('should set, add and delete index patterns in rules', async () => {
const ruleId = 'ruleId';
const indices = ['index1-*', 'index2-*'];
await createRule(supertest, log, getSimpleRule(ruleId));
const { body: setIndexBody } = await postBulkAction()
.send({
query: '',
action: BulkAction.edit,
[BulkAction.edit]: [
{
type: BulkActionEditType.set_index_patterns,
value: ['initial-index-*'],
},
],
})
.expect(200);
expect(setIndexBody.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 });
// Check that the updated rule is returned with the response
expect(setIndexBody.attributes.results.updated[0].index).to.eql(['initial-index-*']);
// Check that the updates have been persisted
const { body: setIndexRule } = await fetchRule(ruleId).expect(200);
expect(setIndexRule.index).to.eql(['initial-index-*']);
const { body: addIndexBody } = await postBulkAction()
.send({
query: '',
action: BulkAction.edit,
[BulkAction.edit]: [
{
type: BulkActionEditType.add_index_patterns,
value: indices,
},
],
})
.expect(200);
expect(addIndexBody.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 });
// Check that the updated rule is returned with the response
expect(addIndexBody.attributes.results.updated[0].index).to.eql([
'initial-index-*',
...indices,
]);
// Check that the updates have been persisted
const { body: addIndexRule } = await fetchRule(ruleId).expect(200);
expect(addIndexRule.index).to.eql(['initial-index-*', ...indices]);
await postBulkAction()
.send({
query: '',
action: BulkAction.edit,
[BulkAction.edit]: [
{
type: BulkActionEditType.delete_index_patterns,
value: ['index1-*'],
},
],
})
.expect(200);
const { body: deleteIndexRule } = await fetchRule(ruleId).expect(200);
expect(deleteIndexRule.index).to.eql(['initial-index-*', 'index2-*']);
});
it('should set timeline values in rule', async () => {
it('should set timeline template values in rule', async () => {
const ruleId = 'ruleId';
const timelineId = '91832785-286d-4ebe-b884-1a208d111a70';
const timelineTitle = 'Test timeline';
@ -597,7 +862,7 @@ export default ({ getService }: FtrProviderContext): void => {
expect(rule.timeline_title).to.eql(timelineTitle);
});
it('should correctly remove timeline', async () => {
it('should correctly remove timeline template', async () => {
const timelineId = 'test-id';
const timelineTitle = 'Test timeline template';
const ruleId = 'ruleId';
@ -676,68 +941,6 @@ export default ({ getService }: FtrProviderContext): void => {
});
});
it('should return error if index patterns action is applied to machine learning rule', async () => {
const mlRule = await createRule(supertest, log, getSimpleMlRule());
const { body } = await postBulkAction()
.send({
ids: [mlRule.id],
action: BulkAction.edit,
[BulkAction.edit]: [
{
type: BulkActionEditType.add_index_patterns,
value: ['index-*'],
},
],
})
.expect(500);
expect(body.attributes.summary).to.eql({ failed: 1, succeeded: 0, total: 1 });
expect(body.attributes.errors[0]).to.eql({
message:
"Index patterns can't be added. Machine learning rule doesn't have index patterns property",
status_code: 500,
rules: [
{
id: mlRule.id,
name: mlRule.name,
},
],
});
});
it('should return error if all index patterns removed from a rule', async () => {
const rule = await createRule(supertest, log, {
...getSimpleRule(),
index: ['simple-index-*'],
});
const { body } = await postBulkAction()
.send({
ids: [rule.id],
action: BulkAction.edit,
[BulkAction.edit]: [
{
type: BulkActionEditType.delete_index_patterns,
value: ['simple-index-*'],
},
],
})
.expect(500);
expect(body.attributes.summary).to.eql({ failed: 1, succeeded: 0, total: 1 });
expect(body.attributes.errors[0]).to.eql({
message: "Mutated params invalid: Index patterns can't be empty",
status_code: 500,
rules: [
{
id: rule.id,
name: rule.name,
},
],
});
});
it('should increment version on rule bulk edit', async () => {
const ruleId = 'ruleId';
const rule = await createRule(supertest, log, getSimpleRule(ruleId));