diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_actions.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_actions.cy.ts new file mode 100644 index 000000000000..6a3718baa226 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_actions.cy.ts @@ -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); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule.cy.ts index a594d7f3ffc2..b81806e2ce65 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule.cy.ts @@ -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 }); diff --git a/x-pack/plugins/security_solution/cypress/screens/common/controls.ts b/x-pack/plugins/security_solution/cypress/screens/common/controls.ts index b0d6f6c22bce..f312d0d28619 100644 --- a/x-pack/plugins/security_solution/cypress/screens/common/controls.ts +++ b/x-pack/plugins/security_solution/cypress/screens/common/controls.ts @@ -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'; diff --git a/x-pack/plugins/security_solution/cypress/screens/common/rule_actions.ts b/x-pack/plugins/security_solution/cypress/screens/common/rule_actions.ts new file mode 100644 index 000000000000..2fe606fc6bf6 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/screens/common/rule_actions.ts @@ -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}"]`; diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts index 96928ff49da4..85f91188e653 100644 --- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts @@ -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']"; diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index 0c834c140061..853e6036aac1 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -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"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/rules_bulk_edit.ts b/x-pack/plugins/security_solution/cypress/screens/rules_bulk_edit.ts index 6f4056034a05..f31d8ca86a82 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rules_bulk_edit.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rules_bulk_edit.ts @@ -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"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts index 8be6f2b26dfb..ba740e5f74fa 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts @@ -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 }); }; @@ -413,3 +417,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(); +}; diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/connectors.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/connectors.ts new file mode 100644 index 000000000000..a89e578764ee --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/connectors.ts @@ -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) => + 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); diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts index 1909030b726d..82eb2f9255b0 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts @@ -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, diff --git a/x-pack/plugins/security_solution/cypress/tasks/common/rule_actions.ts b/x-pack/plugins/security_solution/cypress/tasks/common/rule_actions.ts new file mode 100644 index 000000000000..2c289eea0f73 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/tasks/common/rule_actions.ts @@ -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, + }); +}; diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index 8f5917222071..e2b0b1bca56f 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -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); diff --git a/x-pack/plugins/security_solution/cypress/tasks/edit_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/edit_rule.ts index 42d5619c28a6..a016691328ff 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/edit_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/edit_rule.ts @@ -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); +}; diff --git a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts index 0bd06911acf7..a412675ab533 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts @@ -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(); +}; diff --git a/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts b/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts index 0000a84f2668..2f6863dbd3a1 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts @@ -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); +}; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/rule_actions_form.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/rule_actions_form.tsx index 0ec448a1ab1f..ec7d81e8b08a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/rule_actions_form.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/rule_actions_form.tsx @@ -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, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx index 368ffd77af58..17443855a6ab 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx @@ -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, }, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 0393fcf239f7..53d65e15a468 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -616,6 +616,7 @@ const RuleDetailsPageComponent: React.FC = ({ } return ( { 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) => + (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(); - - 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: '{}' }, + [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 { 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', () => {