[Security Solution][Detections] increases coverage of bulk edit rules action according to test plan (#141929) (#142978)

## Summary

- adds missing tests according to [test plan](https://docs.google.com/document/d/116x7ITTTJQ6cTiwaGK831_f6Ox7XB3qyLiHxC3Cmf8w/edit#heading=h.tzaw2977z8ue) (internal document)
- small refactoring of cypress tasks related to rule actions: create Email connector moved to common tasks from create_rule screen, as it used in bulk editing as well

(cherry picked from commit d07301fcb6)

Co-authored-by: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2022-10-10 07:44:08 -06:00 committed by GitHub
parent 2ed57765c6
commit da2bd15e9d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 670 additions and 194 deletions

View file

@ -0,0 +1,217 @@
/*
* 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 { ROLES } from '../../../common/test';
import {
RULES_BULK_EDIT_ACTIONS_INFO,
RULES_BULK_EDIT_ACTIONS_WARNING,
ADD_RULE_ACTIONS_MENU_ITEM,
} from '../../screens/rules_bulk_edit';
import { actionFormSelector } from '../../screens/common/rule_actions';
import { cleanKibana, deleteAlertsAndRules, deleteConnectors } from '../../tasks/common';
import {
addSlackRuleAction,
assertSlackRuleAction,
addEmailConnectorAndRuleAction,
assertEmailRuleAction,
} from '../../tasks/common/rule_actions';
import {
waitForRulesTableToBeLoaded,
selectNumberOfRules,
loadPrebuiltDetectionRulesFromHeaderBtn,
goToEditRuleActionsSettingsOf,
} from '../../tasks/alerts_detection_rules';
import {
waitForBulkEditActionToFinish,
submitBulkEditForm,
checkOverwriteRuleActionsCheckbox,
openBulkEditRuleActionsForm,
pickActionFrequency,
openBulkActionsMenu,
} from '../../tasks/rules_bulk_edit';
import { assertSelectedActionFrequency } from '../../tasks/edit_rule';
import { login, visitWithoutDateRange } from '../../tasks/login';
import { esArchiverResetKibana } from '../../tasks/es_archiver';
import { SECURITY_DETECTIONS_RULES_URL } from '../../urls/navigation';
import {
createMachineLearningRule,
createCustomIndicatorRule,
createEventCorrelationRule,
createThresholdRule,
createNewTermsRule,
createSavedQueryRule,
createCustomRuleEnabled,
} from '../../tasks/api_calls/rules';
import { createSlackConnector } from '../../tasks/api_calls/connectors';
import {
getEqlRule,
getNewThreatIndicatorRule,
getNewRule,
getNewThresholdRule,
getMachineLearningRule,
getNewTermsRule,
} from '../../objects/rule';
const ruleNameToAssert = 'Custom rule name with actions';
const expectedNumberOfCustomRulesToBeEdited = 7;
// 7 custom rules of different types + 3 prebuilt.
// number of selected rules doesn't matter, we only want to make sure they will be edited an no modal window displayed as for other actions
const expectedNumberOfRulesToBeEdited = expectedNumberOfCustomRulesToBeEdited + 3;
const expectedExistingSlackMessage = 'Existing slack action';
const expectedSlackMessage = 'Slack action test message';
describe('Detection rules, bulk edit of rule actions', () => {
before(() => {
cleanKibana();
login();
});
beforeEach(() => {
deleteAlertsAndRules();
deleteConnectors();
esArchiverResetKibana();
createSlackConnector().then(({ body }) => {
const actions = [
{
id: body.id,
action_type_id: '.slack',
group: 'default',
params: {
message: expectedExistingSlackMessage,
},
},
];
createCustomRuleEnabled(
{
...getNewRule(),
name: ruleNameToAssert,
},
'1',
'100m',
500,
actions
);
});
createEventCorrelationRule(getEqlRule(), '2');
createMachineLearningRule(getMachineLearningRule(), '3');
createCustomIndicatorRule(getNewThreatIndicatorRule(), '4');
createThresholdRule(getNewThresholdRule(), '5');
createNewTermsRule(getNewTermsRule(), '6');
createSavedQueryRule({ ...getNewRule(), savedId: 'mocked' }, '7');
createSlackConnector();
});
context('Restricted action privileges', () => {
it("User with no privileges can't add rule actions", () => {
login(ROLES.hunter_no_actions);
visitWithoutDateRange(SECURITY_DETECTIONS_RULES_URL, ROLES.hunter_no_actions);
waitForRulesTableToBeLoaded();
selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited);
openBulkActionsMenu();
cy.get(ADD_RULE_ACTIONS_MENU_ITEM).should('be.disabled');
});
});
context('All actions privileges', () => {
beforeEach(() => {
visitWithoutDateRange(SECURITY_DETECTIONS_RULES_URL);
waitForRulesTableToBeLoaded();
});
it('Add a rule action to rules (existing connector)', () => {
const expectedActionFrequency = 'Daily';
loadPrebuiltDetectionRulesFromHeaderBtn();
// select both custom and prebuilt rules
selectNumberOfRules(expectedNumberOfRulesToBeEdited);
openBulkEditRuleActionsForm();
// ensure rule actions info callout displayed on the form
cy.get(RULES_BULK_EDIT_ACTIONS_INFO).should('be.visible');
pickActionFrequency(expectedActionFrequency);
addSlackRuleAction(expectedSlackMessage);
submitBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfRulesToBeEdited });
// check if rule has been updated
goToEditRuleActionsSettingsOf(ruleNameToAssert);
assertSelectedActionFrequency(expectedActionFrequency);
assertSlackRuleAction(expectedExistingSlackMessage, 0);
assertSlackRuleAction(expectedSlackMessage, 1);
// ensure there is no third action
cy.get(actionFormSelector(2)).should('not.exist');
});
it('Overwrite rule actions in rules', () => {
const expectedActionFrequency = 'On each rule execution';
loadPrebuiltDetectionRulesFromHeaderBtn();
// select both custom and prebuilt rules
selectNumberOfRules(expectedNumberOfRulesToBeEdited);
openBulkEditRuleActionsForm();
pickActionFrequency(expectedActionFrequency);
addSlackRuleAction(expectedSlackMessage);
// check overwrite box, ensure warning is displayed
checkOverwriteRuleActionsCheckbox();
cy.get(RULES_BULK_EDIT_ACTIONS_WARNING).contains(
`You're about to overwrite rule actions for ${expectedNumberOfRulesToBeEdited} selected rules`
);
submitBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfRulesToBeEdited });
// check if rule has been updated
goToEditRuleActionsSettingsOf(ruleNameToAssert);
assertSelectedActionFrequency(expectedActionFrequency);
assertSlackRuleAction(expectedSlackMessage);
// ensure existing action was overwritten
cy.get(actionFormSelector(1)).should('not.exist');
});
it('Add a rule action to rules (new connector)', () => {
const expectedActionFrequency = 'Hourly';
const expectedEmail = 'test@example.com';
const expectedSubject = 'Subject';
selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited);
openBulkEditRuleActionsForm();
pickActionFrequency(expectedActionFrequency);
addEmailConnectorAndRuleAction(expectedEmail, expectedSubject);
submitBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited });
// check if rule has been updated
goToEditRuleActionsSettingsOf(ruleNameToAssert);
assertSelectedActionFrequency(expectedActionFrequency);
assertEmailRuleAction(expectedEmail, expectedSubject);
});
});
});

View file

@ -39,16 +39,10 @@ import {
RULE_NAME_INPUT,
SCHEDULE_INTERVAL_AMOUNT_INPUT,
SCHEDULE_INTERVAL_UNITS_INPUT,
SCHEDULE_CONTINUE_BUTTON,
SEVERITY_DROPDOWN,
TAGS_CLEAR_BUTTON,
TAGS_FIELD,
EMAIL_ACTION_BTN,
CREATE_ACTION_CONNECTOR_BTN,
SAVE_ACTION_CONNECTOR_BTN,
FROM_VALIDATION_ERROR,
EMAIL_ACTION_TO_INPUT,
EMAIL_ACTION_SUBJECT_INPUT,
SCHEDULE_CONTINUE_BUTTON,
} from '../../screens/create_new_rule';
import {
ADDITIONAL_LOOK_BACK_DETAILS,
@ -86,12 +80,12 @@ import {
import { createCustomRuleEnabled } from '../../tasks/api_calls/rules';
import { createTimeline } from '../../tasks/api_calls/timelines';
import { cleanKibana, deleteAlertsAndRules } from '../../tasks/common';
import { addEmailConnectorAndRuleAction } from '../../tasks/common/rule_actions';
import {
createAndEnableRule,
fillAboutRule,
fillAboutRuleAndContinue,
fillDefineCustomRuleAndContinue,
fillEmailConnectorForm,
fillScheduleRuleAndContinue,
goToAboutStepTab,
goToActionsStepTab,
@ -377,15 +371,8 @@ describe('Custom query rules', () => {
cy.get(ACTIONS_THROTTLE_INPUT).invoke('val').should('eql', 'no_actions');
cy.get(ACTIONS_THROTTLE_INPUT).select('Weekly');
cy.get(EMAIL_ACTION_BTN).click();
cy.get(CREATE_ACTION_CONNECTOR_BTN).click();
fillEmailConnectorForm();
cy.get(SAVE_ACTION_CONNECTOR_BTN).click();
cy.get(EMAIL_ACTION_TO_INPUT).type('test@example.com');
cy.get(EMAIL_ACTION_SUBJECT_INPUT).type('Subject');
cy.get(FROM_VALIDATION_ERROR).should('not.exist');
addEmailConnectorAndRuleAction('test@example.com', 'Subject');
goToAboutStepTab();
cy.get(TAGS_CLEAR_BUTTON).click({ force: true });

View file

@ -10,3 +10,7 @@ export const TIMELINE_SEARCHBOX = '[data-test-subj="timeline-super-select-search
export const EUI_FILTER_SELECT_ITEM = '.euiFilterSelectItem';
export const EUI_CHECKBOX = '.euiCheckbox__input';
export const COMBO_BOX_INPUT = '[data-test-subj="comboBoxInput"]';
export const COMBO_BOX_SELECTION = '.euiMark';

View file

@ -0,0 +1,43 @@
/*
* 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 EMAIL_ACTION_BTN = '[data-test-subj=".email-siem-ActionTypeSelectOption"]';
export const CREATE_ACTION_CONNECTOR_BTN = '[data-test-subj="createActionConnectorButton-0"]';
export const SAVE_ACTION_CONNECTOR_BTN = '[data-test-subj="saveActionButtonModal"]';
export const EMAIL_ACTION_TO_INPUT = '[data-test-subj="toEmailAddressInput"]';
export const EMAIL_ACTION_SUBJECT_INPUT = '[data-test-subj="subjectInput"]';
export const SLACK_ACTION_BTN = '[data-test-subj=".slack-siem-ActionTypeSelectOption"]';
export const SLACK_ACTION_MESSAGE_TEXTAREA = '[data-test-subj="messageTextArea"]';
export const CONNECTOR_NAME_INPUT = '[data-test-subj="nameInput"]';
export const EMAIL_CONNECTOR_FROM_INPUT = '[data-test-subj="emailFromInput"]';
export const EMAIL_CONNECTOR_HOST_INPUT = '[data-test-subj="emailHostInput"]';
export const EMAIL_CONNECTOR_PORT_INPUT = '[data-test-subj="emailPortInput"]';
export const EMAIL_CONNECTOR_USER_INPUT = '[data-test-subj="emailUserInput"]';
export const EMAIL_CONNECTOR_PASSWORD_INPUT = '[data-test-subj="emailPasswordInput"]';
export const EMAIL_CONNECTOR_SERVICE_SELECTOR = '[data-test-subj="emailServiceSelectInput"]';
export const FORM_VALIDATION_ERROR = '.euiFormErrorText';
export const JSON_EDITOR = "[data-test-subj='actionJsonEditor']";
export const INDEX_SELECTOR = "[data-test-subj='.index-siem-ActionTypeSelectOption']";
export const actionFormSelector = (position: number) =>
`[data-test-subj="alertActionAccordion-${position}"]`;

View file

@ -16,34 +16,6 @@ export const ACTIONS_EDIT_TAB = '[data-test-subj="edit-rule-actions-tab"]';
export const ACTIONS_THROTTLE_INPUT =
'[data-test-subj="stepRuleActions"] [data-test-subj="select"]';
export const EMAIL_ACTION_BTN = '[data-test-subj=".email-siem-ActionTypeSelectOption"]';
export const CREATE_ACTION_CONNECTOR_BTN = '[data-test-subj="createActionConnectorButton-0"]';
export const SAVE_ACTION_CONNECTOR_BTN = '[data-test-subj="saveActionButtonModal"]';
export const EMAIL_ACTION_TO_INPUT = '[data-test-subj="toEmailAddressInput"]';
export const EMAIL_ACTION_SUBJECT_INPUT = '[data-test-subj="subjectInput"]';
export const FROM_VALIDATION_ERROR = '.euiFormErrorText';
export const CONNECTOR_NAME_INPUT = '[data-test-subj="nameInput"]';
export const EMAIL_CONNECTOR_FROM_INPUT = '[data-test-subj="emailFromInput"]';
export const EMAIL_CONNECTOR_HOST_INPUT = '[data-test-subj="emailHostInput"]';
export const EMAIL_CONNECTOR_PORT_INPUT = '[data-test-subj="emailPortInput"]';
export const EMAIL_CONNECTOR_USER_INPUT = '[data-test-subj="emailUserInput"]';
export const EMAIL_CONNECTOR_PASSWORD_INPUT = '[data-test-subj="emailPasswordInput"]';
export const EMAIL_CONNECTOR_SERVICE_SELECTOR = '[data-test-subj="emailServiceSelectInput"]';
export const JSON_EDITOR = "[data-test-subj='actionJsonEditor']";
export const ADD_FALSE_POSITIVE_BTN =
'[data-test-subj="detectionEngineStepAboutRuleFalsePositives"] .euiButtonEmpty__text';
@ -58,10 +30,6 @@ export const BACK_TO_ALL_RULES_LINK = '[data-test-subj="ruleDetailsBackToAllRule
export const COMBO_BOX_CLEAR_BTN = '[data-test-subj="comboBoxClearButton"]';
export const COMBO_BOX_INPUT = '[data-test-subj="comboBoxInput"]';
export const COMBO_BOX_SELECTION = '.euiMark';
export const CREATE_AND_ENABLE_BTN = '[data-test-subj="create-enable"]';
export const CUSTOM_QUERY_INPUT = '[data-test-subj="queryInput"]';
@ -260,7 +228,3 @@ export const savedQueryByName = (savedQueryName: string) =>
export const APPLY_SELECTED_SAVED_QUERY_BUTTON =
'[data-test-subj="saved-query-management-apply-changes-button"]';
export const INDEX_SELECTOR = "[data-test-subj='.index-siem-ActionTypeSelectOption']";
export const CREATE_CONNECTOR_BTN = "[data-test-subj='createActionConnectorButton-0']";

View file

@ -123,3 +123,5 @@ export const BACK_TO_RULES = '[data-test-subj="ruleDetailsBackToAllRules"]';
export const DEFINE_RULE_PANEL_PROGRESS =
'[data-test-subj="defineRule"] [data-test-subj="stepPanelProgress"]';
export const EDIT_RULE_SETTINGS_LINK = '[data-test-subj="editRuleSettingsLink"]';

View file

@ -15,6 +15,8 @@ export const DELETE_INDEX_PATTERNS_RULE_BULK_MENU_ITEM =
export const UPDATE_SCHEDULE_MENU_ITEM = '[data-test-subj="setScheduleBulk"]';
export const ADD_RULE_ACTIONS_MENU_ITEM = '[data-test-subj="addRuleActionsBulk"]';
export const APPLY_TIMELINE_RULE_BULK_MENU_ITEM = '[data-test-subj="applyTimelineTemplateBulk"]';
export const TAGS_RULE_BULK_MENU_ITEM = '[data-test-subj="tagsBulkEditRule"]';
@ -65,3 +67,13 @@ export const UPDATE_SCHEDULE_LOOKBACK_INPUT =
export const UPDATE_SCHEDULE_TIME_INTERVAL = '[data-test-subj="interval"]';
export const UPDATE_SCHEDULE_TIME_UNIT_SELECT = '[data-test-subj="timeType"]';
export const RULES_BULK_EDIT_ACTIONS_THROTTLE_INPUT =
'[data-test-subj="bulkEditRulesRuleActionThrottle"] [data-test-subj="select"]';
export const RULES_BULK_EDIT_ACTIONS_INFO = '[data-test-subj="bulkEditRulesRuleActionInfo"]';
export const RULES_BULK_EDIT_ACTIONS_WARNING = '[data-test-subj="bulkEditRulesRuleActionsWarning"]';
export const RULES_BULK_EDIT_OVERWRITE_ACTIONS_CHECKBOX =
'[data-test-subj="bulkEditRulesOverwriteRuleActions"]';

View file

@ -59,8 +59,12 @@ import {
} from '../screens/alerts_detection_rules';
import { EUI_CHECKBOX } from '../screens/common/controls';
import { ALL_ACTIONS } from '../screens/rule_details';
import { EDIT_SUBMIT_BUTTON } from '../screens/edit_rule';
import { LOADING_INDICATOR } from '../screens/security_header';
import { goToRuleEditSettings } from './rule_details';
import { goToActionsStepTab } from './create_new_rule';
export const enableRule = (rulePosition: number) => {
cy.get(RULE_SWITCH).eq(rulePosition).click({ force: true });
};
@ -387,3 +391,11 @@ export const cancelConfirmationModal = () => {
export const clickErrorToastBtn = () => {
cy.get(TOASTER_ERROR_BTN).click();
};
export const goToEditRuleActionsSettingsOf = (name: string) => {
goToTheRuleDetailsOf(name);
goToRuleEditSettings();
// wait until first step loads completely. Otherwise cypress stuck at the first edit page
cy.get(EDIT_SUBMIT_BUTTON).should('be.enabled');
goToActionsStepTab();
};

View file

@ -0,0 +1,24 @@
/*
* 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 createConnector = (connector: Record<string, unknown>) =>
cy.request({
method: 'POST',
url: '/api/actions/action',
body: connector,
headers: { 'kbn-xsrf': 'cypress-creds' },
});
const slackConnectorAPIPayload = {
actionTypeId: '.slack',
secrets: {
webhookUrl: 'http://localhost:123',
},
name: 'Slack cypress test e2e connector',
};
export const createSlackConnector = () => createConnector(slackConnectorAPIPayload);

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import type { Actions } from '@kbn/securitysolution-io-ts-alerting-types';
import type {
CustomRule,
ThreatIndicatorRule,
@ -70,6 +72,7 @@ export const createCustomRule = (
timeline_title: timeline.title,
}
: {}),
actions: rule.actions,
},
headers: { 'kbn-xsrf': 'cypress-creds' },
failOnStatusCode: false,
@ -254,7 +257,8 @@ export const createCustomRuleEnabled = (
rule: CustomRule,
ruleId = '1',
interval = '100m',
maxSignals = 500
maxSignals = 500,
actions?: Actions
) => {
const riskScore = rule.riskScore != null ? parseInt(rule.riskScore, 10) : undefined;
const severity = rule.severity != null ? rule.severity.toLocaleLowerCase() : undefined;
@ -280,6 +284,7 @@ export const createCustomRuleEnabled = (
tags: ['rule1'],
max_signals: maxSignals,
building_block_type: rule.buildingBlockType,
actions,
},
headers: { 'kbn-xsrf': 'cypress-creds' },
failOnStatusCode: false,
@ -306,6 +311,7 @@ export const createCustomRuleEnabled = (
tags: ['rule1'],
max_signals: maxSignals,
building_block_type: rule.buildingBlockType,
actions,
},
headers: { 'kbn-xsrf': 'cypress-creds' },
failOnStatusCode: false,

View file

@ -0,0 +1,86 @@
/*
* 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 {
SLACK_ACTION_BTN,
SLACK_ACTION_MESSAGE_TEXTAREA,
EMAIL_ACTION_BTN,
CREATE_ACTION_CONNECTOR_BTN,
SAVE_ACTION_CONNECTOR_BTN,
EMAIL_ACTION_TO_INPUT,
EMAIL_ACTION_SUBJECT_INPUT,
CONNECTOR_NAME_INPUT,
EMAIL_CONNECTOR_SERVICE_SELECTOR,
EMAIL_CONNECTOR_FROM_INPUT,
EMAIL_CONNECTOR_HOST_INPUT,
EMAIL_CONNECTOR_PORT_INPUT,
EMAIL_CONNECTOR_USER_INPUT,
EMAIL_CONNECTOR_PASSWORD_INPUT,
FORM_VALIDATION_ERROR,
JSON_EDITOR,
} from '../../screens/common/rule_actions';
import { COMBO_BOX_INPUT, COMBO_BOX_SELECTION } from '../../screens/common/controls';
import type { EmailConnector, IndexConnector } from '../../objects/connector';
import { getEmailConnector, getIndexConnector } from '../../objects/connector';
export const addSlackRuleAction = (message: string) => {
cy.get(SLACK_ACTION_BTN).click();
cy.get(SLACK_ACTION_MESSAGE_TEXTAREA).clear().type(message);
};
export const assertSlackRuleAction = (message: string, position: number = 0) => {
cy.get(SLACK_ACTION_MESSAGE_TEXTAREA).eq(position).should('have.value', message);
};
export const fillEmailConnectorForm = (connector: EmailConnector = getEmailConnector()) => {
cy.get(CONNECTOR_NAME_INPUT).type(connector.name);
cy.get(EMAIL_CONNECTOR_SERVICE_SELECTOR).select(connector.service);
cy.get(EMAIL_CONNECTOR_FROM_INPUT).type(connector.from);
cy.get(EMAIL_CONNECTOR_HOST_INPUT).type(connector.host);
cy.get(EMAIL_CONNECTOR_PORT_INPUT).type(connector.port);
cy.get(EMAIL_CONNECTOR_USER_INPUT).type(connector.user);
cy.get(EMAIL_CONNECTOR_PASSWORD_INPUT).type(connector.password);
};
export const createEmailConnector = () => {
cy.get(CREATE_ACTION_CONNECTOR_BTN).click();
fillEmailConnectorForm();
cy.get(SAVE_ACTION_CONNECTOR_BTN).click();
};
export const fillEmailRuleActionForm = (email: string, subject: string) => {
cy.get(EMAIL_ACTION_TO_INPUT).type(email);
cy.get(EMAIL_ACTION_SUBJECT_INPUT).type(subject);
};
export const addEmailConnectorAndRuleAction = (email: string, subject: string) => {
cy.get(EMAIL_ACTION_BTN).click();
createEmailConnector();
fillEmailRuleActionForm(email, subject);
cy.get(FORM_VALIDATION_ERROR).should('not.exist');
};
export const assertEmailRuleAction = (email: string, subject: string) => {
cy.get(EMAIL_ACTION_TO_INPUT).contains(email);
cy.get(EMAIL_ACTION_SUBJECT_INPUT).should('have.value', subject);
};
export const fillIndexConnectorForm = (connector: IndexConnector = getIndexConnector()) => {
cy.get(CONNECTOR_NAME_INPUT).type(connector.name);
cy.get(COMBO_BOX_INPUT).type(connector.index);
cy.get(COMBO_BOX_SELECTION).click({ force: true });
cy.get(SAVE_ACTION_CONNECTOR_BTN).click();
cy.get(SAVE_ACTION_CONNECTOR_BTN).should('not.exist');
cy.get(JSON_EDITOR).should('be.visible');
cy.get(JSON_EDITOR).click();
cy.get(JSON_EDITOR).type(connector.document, {
parseSpecialCharSequences: false,
});
};

View file

@ -5,8 +5,6 @@
* 2.0.
*/
import type { EmailConnector, IndexConnector } from '../objects/connector';
import { getIndexConnector, getEmailConnector } from '../objects/connector';
import type {
CustomRule,
MachineLearningRule,
@ -30,7 +28,6 @@ import {
AT_LEAST_ONE_VALID_MATCH,
BACK_TO_ALL_RULES_LINK,
COMBO_BOX_CLEAR_BTN,
COMBO_BOX_INPUT,
CREATE_AND_ENABLE_BTN,
CUSTOM_QUERY_INPUT,
CUSTOM_QUERY_REQUIRED,
@ -93,13 +90,6 @@ import {
THREAT_MATCH_QUERY_INPUT,
THRESHOLD_INPUT_AREA,
THRESHOLD_TYPE,
CONNECTOR_NAME_INPUT,
EMAIL_CONNECTOR_FROM_INPUT,
EMAIL_CONNECTOR_HOST_INPUT,
EMAIL_CONNECTOR_PORT_INPUT,
EMAIL_CONNECTOR_USER_INPUT,
EMAIL_CONNECTOR_PASSWORD_INPUT,
EMAIL_CONNECTOR_SERVICE_SELECTOR,
PREVIEW_HISTOGRAM,
DATA_VIEW_COMBO_BOX,
DATA_VIEW_OPTION,
@ -108,19 +98,18 @@ import {
NEW_TERMS_HISTORY_TIME_TYPE,
NEW_TERMS_INPUT_AREA,
ACTIONS_THROTTLE_INPUT,
} from '../screens/create_new_rule';
import {
INDEX_SELECTOR,
CREATE_CONNECTOR_BTN,
SAVE_ACTION_CONNECTOR_BTN,
JSON_EDITOR,
CREATE_ACTION_CONNECTOR_BTN,
EMAIL_ACTION_BTN,
COMBO_BOX_SELECTION,
} from '../screens/create_new_rule';
} from '../screens/common/rule_actions';
import { fillIndexConnectorForm, fillEmailConnectorForm } from './common/rule_actions';
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';
import { EUI_FILTER_SELECT_ITEM, COMBO_BOX_INPUT } from '../screens/common/controls';
export const createAndEnableRule = () => {
cy.get(CREATE_AND_ENABLE_BTN).click({ force: true });
@ -327,7 +316,7 @@ export const fillRuleAction = (rule: CustomRule) => {
switch (connector.type) {
case 'index':
cy.get(INDEX_SELECTOR).click();
cy.get(CREATE_CONNECTOR_BTN).click();
cy.get(CREATE_ACTION_CONNECTOR_BTN).click();
fillIndexConnectorForm(connector);
break;
case 'email':
@ -487,31 +476,6 @@ export const fillIndexAndIndicatorIndexPattern = (
getIndicatorIndicatorIndex().type(`{backspace}{enter}${indicatorIndex}{enter}`);
};
export const fillEmailConnectorForm = (connector: EmailConnector = getEmailConnector()) => {
cy.get(CONNECTOR_NAME_INPUT).type(connector.name);
cy.get(EMAIL_CONNECTOR_SERVICE_SELECTOR).select(connector.service);
cy.get(EMAIL_CONNECTOR_FROM_INPUT).type(connector.from);
cy.get(EMAIL_CONNECTOR_HOST_INPUT).type(connector.host);
cy.get(EMAIL_CONNECTOR_PORT_INPUT).type(connector.port);
cy.get(EMAIL_CONNECTOR_USER_INPUT).type(connector.user);
cy.get(EMAIL_CONNECTOR_PASSWORD_INPUT).type(connector.password);
};
export const fillIndexConnectorForm = (connector: IndexConnector = getIndexConnector()) => {
cy.get(CONNECTOR_NAME_INPUT).type(connector.name);
cy.get(COMBO_BOX_INPUT).type(connector.index);
cy.get(COMBO_BOX_SELECTION).click({ force: true });
cy.get(SAVE_ACTION_CONNECTOR_BTN).click();
cy.get(SAVE_ACTION_CONNECTOR_BTN).should('not.exist');
cy.get(JSON_EDITOR).should('be.visible');
cy.get(JSON_EDITOR).click();
cy.get(JSON_EDITOR).type(connector.document, {
parseSpecialCharSequences: false,
});
};
/** Returns the indicator index drop down field. Pass in row number, default is 1 */
export const getIndicatorIndexComboField = (row = 1) =>
cy.get(THREAT_COMBO_BOX_INPUT).eq(row * 2 - 2);

View file

@ -6,6 +6,7 @@
*/
import { BACK_TO_RULE_DETAILS, EDIT_SUBMIT_BUTTON } from '../screens/edit_rule';
import { ACTIONS_THROTTLE_INPUT } from '../screens/create_new_rule';
export const saveEditedRule = () => {
cy.get(EDIT_SUBMIT_BUTTON).should('exist').click({ force: true });
@ -16,3 +17,7 @@ export const goBackToRuleDetails = () => {
cy.get(BACK_TO_RULE_DETAILS).should('exist').click();
cy.get(BACK_TO_RULE_DETAILS).should('not.exist');
};
export const assertSelectedActionFrequency = (frequency: string) => {
cy.get(ACTIONS_THROTTLE_INPUT).find('option:selected').should('have.text', frequency);
};

View file

@ -32,6 +32,7 @@ import {
DETAILS_DESCRIPTION,
EXCEPTION_ITEM_ACTIONS_BUTTON,
EDIT_EXCEPTION_BTN,
EDIT_RULE_SETTINGS_LINK,
} from '../screens/rule_details';
import { addsFields, closeFieldsBrowser, filterFieldsBrowser } from './fields_browser';
@ -166,3 +167,7 @@ export const hasIndexPatterns = (indexPatterns: string) => {
getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns);
});
};
export const goToRuleEditSettings = () => {
cy.get(EDIT_RULE_SETTINGS_LINK).click();
};

View file

@ -19,6 +19,7 @@ import {
import {
INDEX_PATTERNS_RULE_BULK_MENU_ITEM,
ADD_INDEX_PATTERNS_RULE_BULK_MENU_ITEM,
ADD_RULE_ACTIONS_MENU_ITEM,
DELETE_INDEX_PATTERNS_RULE_BULK_MENU_ITEM,
TAGS_RULE_BULK_MENU_ITEM,
ADD_TAGS_RULE_BULK_MENU_ITEM,
@ -37,6 +38,8 @@ import {
UPDATE_SCHEDULE_TIME_UNIT_SELECT,
UPDATE_SCHEDULE_LOOKBACK_INPUT,
RULES_BULK_EDIT_SCHEDULES_WARNING,
RULES_BULK_EDIT_OVERWRITE_ACTIONS_CHECKBOX,
RULES_BULK_EDIT_ACTIONS_THROTTLE_INPUT,
} from '../screens/rules_bulk_edit';
import { SCHEDULE_DETAILS } from '../screens/rule_details';
@ -81,6 +84,13 @@ export const openBulkEditAddIndexPatternsForm = () => {
cy.get(RULES_BULK_EDIT_FORM_TITLE).should('have.text', 'Add index patterns');
};
export const openBulkEditRuleActionsForm = () => {
cy.get(BULK_ACTIONS_BTN).click();
cy.get(ADD_RULE_ACTIONS_MENU_ITEM).click();
cy.get(RULES_BULK_EDIT_FORM_TITLE).should('have.text', 'Add rule actions');
};
export const openBulkEditDeleteIndexPatternsForm = () => {
cy.get(BULK_ACTIONS_BTN).click();
cy.get(INDEX_PATTERNS_RULE_BULK_MENU_ITEM).click();
@ -160,6 +170,14 @@ export const checkOverwriteIndexPatternsCheckbox = () => {
.should('be.checked');
};
export const checkOverwriteRuleActionsCheckbox = () => {
cy.get(RULES_BULK_EDIT_OVERWRITE_ACTIONS_CHECKBOX)
.should('have.text', 'Overwrite all selected rules actions')
.find('input')
.click({ force: true })
.should('be.checked');
};
export const checkOverwriteDataViewCheckbox = () => {
cy.get(RULES_BULK_EDIT_OVERWRITE_DATA_VIEW_CHECKBOX)
.should('have.text', 'Apply changes to rules configured with data views')
@ -194,6 +212,7 @@ export const typeScheduleInterval = (interval: string) => {
.type(interval.toString())
.blur();
};
export const typeScheduleLookback = (lookback: string) => {
cy.get(UPDATE_SCHEDULE_LOOKBACK_INPUT)
.find('input')
@ -245,3 +264,7 @@ export const assertRuleScheduleValues = ({ interval, lookback }: RuleSchedule) =
cy.get('dd').eq(1).should('contain.text', lookback);
});
};
export const pickActionFrequency = (frequency: string) => {
cy.get(RULES_BULK_EDIT_ACTIONS_THROTTLE_INPUT).select(frequency);
};

View file

@ -122,7 +122,7 @@ const RuleActionsFormComponent = ({ rulesCount, onClose, onConfirm }: RuleAction
const throttleFieldComponentProps = useMemo(
() => ({
idAria: 'bulkEditRulesRuleActionThrottle',
dataTestSubj: 'bulkEditRulesRuleActionThrottle',
'data-test-subj': 'bulkEditRulesRuleActionThrottle',
hasNoInitialSelection: false,
euiFieldProps: {
options: THROTTLE_OPTIONS_FOR_BULK_RULE_ACTIONS,

View file

@ -364,9 +364,9 @@ export const useBulkActions = ({
key: i18n.BULK_ACTION_ADD_RULE_ACTIONS,
name: i18n.BULK_ACTION_ADD_RULE_ACTIONS,
'data-test-subj': 'addRuleActionsBulk',
disabled: isEditDisabled,
disabled: !hasActionsPrivileges || isEditDisabled,
onClick: handleBulkEdit(BulkActionEditType.add_rule_actions),
toolTipContent: missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
toolTipContent: !hasActionsPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
toolTipPosition: 'right',
icon: undefined,
},

View file

@ -616,6 +616,7 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
}
return (
<LinkButton
data-test-subj="editRuleSettingsLink"
onClick={goToEditRule}
iconType="controlsHorizontal"
isDisabled={!isExistingRule || !userHasPermissions(canUserCRUD)}

View file

@ -34,6 +34,7 @@ import {
installPrePackagedRules,
getSimpleMlRule,
getWebHookAction,
getSlackAction,
} from '../../utils';
// eslint-disable-next-line import/no-default-export
@ -63,14 +64,12 @@ export default ({ getService }: FtrProviderContext): void => {
const fetchRuleByAlertApi = (ruleId: string) =>
supertest.get(`/api/alerting/rule/${ruleId}`).set('kbn-xsrf', 'true');
const createWebHookAction = async () =>
(
await supertest
.post('/api/actions/action')
.set('kbn-xsrf', 'true')
.send(getWebHookAction())
.expect(200)
).body;
const createConnector = async (payload: Record<string, unknown>) =>
(await supertest.post('/api/actions/action').set('kbn-xsrf', 'true').send(payload).expect(200))
.body;
const createWebHookConnector = () => createConnector(getWebHookAction());
const createSlackConnector = () => createConnector(getSlackAction());
describe('perform_bulk_action', () => {
beforeEach(async () => {
@ -1092,17 +1091,17 @@ export default ({ getService }: FtrProviderContext): void => {
const webHookActionMock = {
group: 'default',
params: {
body: '{}',
body: '{"test":"action to be saved in a rule"}',
},
};
describe('set_rule_actions', () => {
it('should set action correctly', async () => {
it('should set action correctly to existing empty actions list', async () => {
const ruleId = 'ruleId';
const createdRule = await createRule(supertest, log, getSimpleRule(ruleId));
// create a new action
const hookAction = await createWebHookAction();
// create a new connector
const webHookConnector = await createWebHookConnector();
const { body } = await postBulkAction()
.send({
@ -1116,7 +1115,7 @@ export default ({ getService }: FtrProviderContext): void => {
actions: [
{
...webHookActionMock,
id: hookAction.id,
id: webHookConnector.id,
},
],
},
@ -1125,33 +1124,85 @@ export default ({ getService }: FtrProviderContext): void => {
})
.expect(200);
// Check that the updated rule is returned with the response
expect(body.attributes.results.updated[0].actions).to.eql([
const expectedRuleActions = [
{
...webHookActionMock,
id: hookAction.id,
id: webHookConnector.id,
action_type_id: '.webhook',
},
]);
];
// Check that the updated rule is returned with the response
expect(body.attributes.results.updated[0].actions).to.eql(expectedRuleActions);
// Check that the updates have been persisted
const { body: readRule } = await fetchRule(ruleId).expect(200);
expect(readRule.actions).to.eql([
expect(readRule.actions).to.eql(expectedRuleActions);
});
it('should set action correctly to existing non empty actions list', async () => {
const webHookConnector = await createWebHookConnector();
const existingRuleAction = {
id: webHookConnector.id,
action_type_id: '.webhook',
group: 'default',
params: {
body: '{"test":"an existing action"}',
},
};
const ruleId = 'ruleId';
const createdRule = await createRule(supertest, log, {
...getSimpleRule(ruleId),
actions: [existingRuleAction],
});
const { body } = await postBulkAction()
.send({
ids: [createdRule.id],
action: BulkAction.edit,
[BulkAction.edit]: [
{
type: BulkActionEditType.set_rule_actions,
value: {
throttle: '1h',
actions: [
{
...webHookActionMock,
id: webHookConnector.id,
},
],
},
},
],
})
.expect(200);
const expectedRuleActions = [
{
...webHookActionMock,
id: hookAction.id,
id: webHookConnector.id,
action_type_id: '.webhook',
},
]);
];
// Check that the updated rule is returned with the response
expect(body.attributes.results.updated[0].actions).to.eql(expectedRuleActions);
// Check that the updates have been persisted
const { body: readRule } = await fetchRule(ruleId).expect(200);
expect(readRule.actions).to.eql(expectedRuleActions);
});
it('should set actions to empty list, actions payload is empty list', async () => {
// create a new action
const hookAction = await createWebHookAction();
// create a new connector
const webHookConnector = await createWebHookConnector();
const defaultRuleAction = {
id: hookAction.id,
id: webHookConnector.id,
action_type_id: '.webhook',
group: 'default',
params: {
@ -1197,8 +1248,8 @@ export default ({ getService }: FtrProviderContext): void => {
const ruleId = 'ruleId';
const createdRule = await createRule(supertest, log, getSimpleRule(ruleId));
// create a new action
const hookAction = await createWebHookAction();
// create a new connector
const webHookConnector = await createWebHookConnector();
const { body } = await postBulkAction()
.send({
@ -1212,7 +1263,7 @@ export default ({ getService }: FtrProviderContext): void => {
actions: [
{
...webHookActionMock,
id: hookAction.id,
id: webHookConnector.id,
},
],
},
@ -1221,33 +1272,29 @@ export default ({ getService }: FtrProviderContext): void => {
})
.expect(200);
// Check that the updated rule is returned with the response
expect(body.attributes.results.updated[0].actions).to.eql([
const expectedRuleActions = [
{
...webHookActionMock,
id: hookAction.id,
id: webHookConnector.id,
action_type_id: '.webhook',
},
]);
];
// Check that the updated rule is returned with the response
expect(body.attributes.results.updated[0].actions).to.eql(expectedRuleActions);
// Check that the updates have been persisted
const { body: readRule } = await fetchRule(ruleId).expect(200);
expect(readRule.actions).to.eql([
{
...webHookActionMock,
id: hookAction.id,
action_type_id: '.webhook',
},
]);
expect(readRule.actions).to.eql(expectedRuleActions);
});
it('should add action correctly to non empty actions list', async () => {
// create a new action
const hookAction = await createWebHookAction();
it('should add action correctly to non empty actions list of the same type', async () => {
// create a new connector
const webHookConnector = await createWebHookConnector();
const defaultRuleAction = {
id: hookAction.id,
id: webHookConnector.id,
action_type_id: '.webhook',
group: 'default',
params: {
@ -1274,7 +1321,7 @@ export default ({ getService }: FtrProviderContext): void => {
actions: [
{
...webHookActionMock,
id: hookAction.id,
id: webHookConnector.id,
},
],
},
@ -1283,35 +1330,97 @@ export default ({ getService }: FtrProviderContext): void => {
})
.expect(200);
// Check that the updated rule is returned with the response
expect(body.attributes.results.updated[0].actions).to.eql([
const expectedRuleActions = [
defaultRuleAction,
{
...webHookActionMock,
id: hookAction.id,
id: webHookConnector.id,
action_type_id: '.webhook',
},
]);
];
// Check that the updated rule is returned with the response
expect(body.attributes.results.updated[0].actions).to.eql(expectedRuleActions);
// Check that the updates have been persisted
const { body: readRule } = await fetchRule(ruleId).expect(200);
expect(readRule.actions).to.eql([
expect(readRule.actions).to.eql(expectedRuleActions);
});
it('should add action correctly to non empty actions list of a different type', async () => {
// create new actions
const webHookAction = await createWebHookConnector();
const slackConnector = await createSlackConnector();
const defaultRuleAction = {
id: webHookAction.id,
action_type_id: '.webhook',
group: 'default',
params: {
body: '{"test":"a default action"}',
},
};
const slackConnectorMockProps = {
group: 'default',
params: {
message: 'test slack message',
},
};
const ruleId = 'ruleId';
const createdRule = await createRule(supertest, log, {
...getSimpleRule(ruleId),
actions: [defaultRuleAction],
throttle: '1d',
});
const { body } = await postBulkAction()
.send({
ids: [createdRule.id],
action: BulkAction.edit,
[BulkAction.edit]: [
{
type: BulkActionEditType.add_rule_actions,
value: {
throttle: '1h',
actions: [
{
...slackConnectorMockProps,
id: slackConnector.id,
},
],
},
},
],
})
.expect(200);
const expectedRuleActions = [
defaultRuleAction,
{
...webHookActionMock,
id: hookAction.id,
action_type_id: '.webhook',
...slackConnectorMockProps,
id: slackConnector.id,
action_type_id: '.slack',
},
]);
];
// Check that the updated rule is returned with the response
expect(body.attributes.results.updated[0].actions).to.eql(expectedRuleActions);
// Check that the updates have been persisted
const { body: readRule } = await fetchRule(ruleId).expect(200);
expect(readRule.actions).to.eql(expectedRuleActions);
});
it('should not change actions of rule if empty list of actions added', async () => {
// create a new action
const hookAction = await createWebHookAction();
// create a new connector
const webHookConnector = await createWebHookConnector();
const defaultRuleAction = {
id: hookAction.id,
id: webHookConnector.id,
action_type_id: '.webhook',
group: 'default',
params: {
@ -1352,11 +1461,11 @@ export default ({ getService }: FtrProviderContext): void => {
});
it('should change throttle if actions list in payload is empty', async () => {
// create a new action
const hookAction = await createWebHookAction();
// create a new connector
const webHookConnector = await createWebHookConnector();
const defaultRuleAction = {
id: hookAction.id,
id: webHookConnector.id,
action_type_id: '.webhook',
group: 'default',
params: {
@ -1410,7 +1519,7 @@ export default ({ getService }: FtrProviderContext): void => {
it(`should apply "${type}" rule action to prebuilt rule`, async () => {
await installPrePackagedRules(supertest, log);
const prebuiltRule = await fetchPrebuiltRule();
const hookAction = await createWebHookAction();
const webHookConnector = await createWebHookConnector();
const { body } = await postBulkAction()
.send({
@ -1424,7 +1533,7 @@ export default ({ getService }: FtrProviderContext): void => {
actions: [
{
...webHookActionMock,
id: hookAction.id,
id: webHookConnector.id,
},
],
},
@ -1438,7 +1547,7 @@ export default ({ getService }: FtrProviderContext): void => {
expect(editedRule.actions).to.eql([
{
...webHookActionMock,
id: hookAction.id,
id: webHookConnector.id,
action_type_id: '.webhook',
},
]);
@ -1451,7 +1560,7 @@ export default ({ getService }: FtrProviderContext): void => {
expect(readRule.actions).to.eql([
{
...webHookActionMock,
id: hookAction.id,
id: webHookConnector.id,
action_type_id: '.webhook',
},
]);
@ -1464,7 +1573,7 @@ export default ({ getService }: FtrProviderContext): void => {
it(`should return error if one of edit action is not eligible for prebuilt rule`, async () => {
await installPrePackagedRules(supertest, log);
const prebuiltRule = await fetchPrebuiltRule();
const hookAction = await createWebHookAction();
const webHookConnector = await createWebHookConnector();
const { body } = await postBulkAction()
.send({
@ -1478,7 +1587,7 @@ export default ({ getService }: FtrProviderContext): void => {
actions: [
{
...webHookActionMock,
id: hookAction.id,
id: webHookConnector.id,
},
],
},
@ -1568,46 +1677,58 @@ export default ({ getService }: FtrProviderContext): void => {
payloadThrottle: '1h',
expectedThrottle: '1h',
},
{
payloadThrottle: '1d',
expectedThrottle: '1d',
},
{
payloadThrottle: '7d',
expectedThrottle: '7d',
},
];
casesForNonEmptyActions.forEach(({ payloadThrottle, expectedThrottle }) => {
it(`throttle is set correctly, if payload throttle="${payloadThrottle}" and actions non empty`, async () => {
// create a new action
const hookAction = await createWebHookAction();
[BulkActionEditType.set_rule_actions, BulkActionEditType.add_rule_actions].forEach(
(ruleAction) => {
casesForNonEmptyActions.forEach(({ payloadThrottle, expectedThrottle }) => {
it(`throttle is updated correctly for rule action "${ruleAction}", if payload throttle="${payloadThrottle}" and actions non empty`, async () => {
// create a new connector
const webHookConnector = await createWebHookConnector();
const ruleId = 'ruleId';
const createdRule = await createRule(supertest, log, getSimpleRule(ruleId));
const ruleId = 'ruleId';
const createdRule = await createRule(supertest, log, getSimpleRule(ruleId));
const { body } = await postBulkAction()
.send({
ids: [createdRule.id],
action: BulkAction.edit,
[BulkAction.edit]: [
{
type: BulkActionEditType.set_rule_actions,
value: {
throttle: payloadThrottle,
actions: [
{
id: hookAction.id,
group: 'default',
params: { body: '{}' },
const { body } = await postBulkAction()
.send({
ids: [createdRule.id],
action: BulkAction.edit,
[BulkAction.edit]: [
{
type: BulkActionEditType.set_rule_actions,
value: {
throttle: payloadThrottle,
actions: [
{
id: webHookConnector.id,
group: 'default',
params: { body: '{}' },
},
],
},
],
},
},
],
})
.expect(200);
},
],
})
.expect(200);
// Check that the updated rule is returned with the response
expect(body.attributes.results.updated[0].throttle).to.eql(expectedThrottle);
// Check that the updated rule is returned with the response
expect(body.attributes.results.updated[0].throttle).to.eql(expectedThrottle);
// Check that the updates have been persisted
const { body: rule } = await fetchRule(ruleId).expect(200);
// Check that the updates have been persisted
const { body: rule } = await fetchRule(ruleId).expect(200);
expect(rule.throttle).to.eql(expectedThrottle);
});
});
expect(rule.throttle).to.eql(expectedThrottle);
});
});
}
);
});
describe('notifyWhen', () => {