diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index 966ce3098d6a7..ef9c7f49cb371 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -60,12 +60,15 @@ import { } from '../../tasks/alerts'; import { changeRowsPerPageTo300, + duplicateFirstRule, + duplicateRuleFromMenu, filterByCustomRules, goToCreateNewRule, goToRuleDetails, waitForRulesTableToBeLoaded, } from '../../tasks/alerts_detection_rules'; -import { cleanKibana } from '../../tasks/common'; +import { createCustomIndicatorRule } from '../../tasks/api_calls/rules'; +import { cleanKibana, reload } from '../../tasks/common'; import { createAndActivateRule, fillAboutRuleAndContinue, @@ -92,8 +95,10 @@ import { waitForAlertsToPopulate, waitForTheRuleToBeExecuted, } from '../../tasks/create_new_rule'; +import { waitForKibana } from '../../tasks/edit_rule'; import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; +import { goBackToAllRulesTable } from '../../tasks/rule_details'; import { DETECTIONS_URL, RULE_CREATION } from '../../urls/navigation'; @@ -465,5 +470,30 @@ describe('indicator match', () => { cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', newThreatIndicatorRule.riskScore); }); }); + + describe('Duplicates the indicator rule', () => { + beforeEach(() => { + cleanKibana(); + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); + goToManageAlertsDetectionRules(); + createCustomIndicatorRule(newThreatIndicatorRule); + reload(); + }); + + it('Allows the rule to be duplicated from the table', () => { + waitForKibana(); + duplicateFirstRule(); + cy.contains(RULE_NAME, `${newThreatIndicatorRule.name} [Duplicate]`); + }); + + it('Allows the rule to be duplicated from the edit screen', () => { + waitForKibana(); + goToRuleDetails(); + duplicateRuleFromMenu(); + goBackToAllRulesTable(); + reload(); + cy.contains(RULE_NAME, `${newThreatIndicatorRule.name} [Duplicate]`); + }); + }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts index 68baad7d3d259..30365c9bd4c70 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts @@ -17,6 +17,12 @@ export const DELETE_RULE_ACTION_BTN = '[data-test-subj="deleteRuleAction"]'; export const EDIT_RULE_ACTION_BTN = '[data-test-subj="editRuleAction"]'; +export const DUPLICATE_RULE_ACTION_BTN = '[data-test-subj="duplicateRuleAction"]'; + +export const DUPLICATE_RULE_MENU_PANEL_BTN = '[data-test-subj="rules-details-duplicate-rule"]'; + +export const REFRESH_BTN = '[data-test-subj="refreshRulesAction"] button'; + export const DELETE_RULE_BULK_BTN = '[data-test-subj="deleteRuleBulk"]'; export const ELASTIC_RULES_BTN = '[data-test-subj="showElasticRulesFilterButton"]'; 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 3553889449e6d..529ef4afdfa63 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 @@ -31,6 +31,8 @@ import { RULE_AUTO_REFRESH_IDLE_MODAL_CONTINUE, rowsPerPageSelector, pageSelector, + DUPLICATE_RULE_ACTION_BTN, + DUPLICATE_RULE_MENU_PANEL_BTN, } from '../screens/alerts_detection_rules'; import { ALL_ACTIONS, DELETE_RULE } from '../screens/rule_details'; @@ -45,6 +47,33 @@ export const editFirstRule = () => { cy.get(EDIT_RULE_ACTION_BTN).click(); }; +export const duplicateFirstRule = () => { + cy.get(COLLAPSED_ACTION_BTN).should('be.visible'); + cy.get(COLLAPSED_ACTION_BTN).first().click({ force: true }); + cy.get(DUPLICATE_RULE_ACTION_BTN).should('be.visible'); + cy.get(DUPLICATE_RULE_ACTION_BTN).click(); +}; + +/** + * Duplicates the rule from the menu and does additional + * pipes and checking that the elements are present on the + * page as well as removed when doing the clicks to help reduce + * flake. + */ +export const duplicateRuleFromMenu = () => { + cy.get(ALL_ACTIONS).should('be.visible'); + cy.root() + .pipe(($el) => { + $el.find(ALL_ACTIONS).trigger('click'); + return $el.find(DUPLICATE_RULE_MENU_PANEL_BTN); + }) + .should(($el) => expect($el).to.be.visible); + // Because of a fade effect and fast clicking this can produce more than one click + cy.get(DUPLICATE_RULE_MENU_PANEL_BTN) + .pipe(($el) => $el.trigger('click')) + .should('not.be.visible'); +}; + export const deleteFirstRule = () => { cy.get(COLLAPSED_ACTION_BTN).first().click({ force: true }); cy.get(DELETE_RULE_ACTION_BTN).click(); 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 ab6063f5809c4..99f5bd9c20230 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,7 +5,7 @@ * 2.0. */ -import { CustomRule } from '../../objects/rule'; +import { CustomRule, ThreatIndicatorRule } from '../../objects/rule'; export const createCustomRule = (rule: CustomRule, ruleId = 'rule_testing') => cy.request({ @@ -29,6 +29,44 @@ export const createCustomRule = (rule: CustomRule, ruleId = 'rule_testing') => failOnStatusCode: false, }); +export const createCustomIndicatorRule = (rule: ThreatIndicatorRule, ruleId = 'rule_testing') => + cy.request({ + method: 'POST', + url: 'api/detection_engine/rules', + body: { + rule_id: ruleId, + risk_score: parseInt(rule.riskScore, 10), + description: rule.description, + interval: '10s', + name: rule.name, + severity: rule.severity.toLocaleLowerCase(), + type: 'threat_match', + threat_mapping: [ + { + entries: [ + { + field: rule.indicatorMapping, + type: 'mapping', + value: rule.indicatorMapping, + }, + ], + }, + ], + threat_query: '*:*', + threat_language: 'kuery', + threat_filters: [], + threat_index: ['mock*'], + threat_indicator_path: '', + from: 'now-17520h', + index: ['exceptions-*'], + query: rule.customQuery || '*:*', + language: 'kuery', + enabled: false, + }, + headers: { 'kbn-xsrf': 'cypress-creds' }, + failOnStatusCode: false, + }); + export const createCustomRuleActivated = (rule: CustomRule, ruleId = '1') => cy.request({ method: 'POST', diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.tsx index 3b1f9e620127d..6cc75a3fda03c 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.tsx @@ -8,6 +8,7 @@ import * as H from 'history'; import React, { Dispatch } from 'react'; +import { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request'; import { deleteRules, duplicateRules, @@ -28,6 +29,7 @@ import { track, METRIC_TYPE, TELEMETRY_EVENT } from '../../../../../common/lib/t import * as i18n from '../translations'; import { bucketRulesResponse } from './helpers'; +import { transformOutput } from '../../../../containers/detection_engine/rules/transforms'; export const editRuleAction = (rule: Rule, history: H.History) => { history.push(getEditRuleUrl(rule.id)); @@ -41,7 +43,11 @@ export const duplicateRulesAction = async ( ) => { try { dispatch({ type: 'loadingRuleIds', ids: ruleIds, actionType: 'duplicate' }); - const response = await duplicateRules({ rules }); + const response = await duplicateRules({ + // We cast this back and forth here as the front end types are not really the right io-ts ones + // and the two types conflict with each other. + rules: rules.map((rule) => transformOutput(rule as CreateRulesSchema) as Rule), + }); const { errors } = bucketRulesResponse(response); if (errors.length > 0) { displayErrorToast( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx index d2488bd3d043c..d2eadef48d9c7 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx @@ -67,6 +67,7 @@ export const getActions = ( enabled: (rowItem: Rule) => canEditRuleWithActions(rowItem, actionsPrivileges), }, { + 'data-test-subj': 'duplicateRuleAction', description: i18n.DUPLICATE_RULE, icon: 'copy', name: !actionsPrivileges ? (