diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rule_fiters.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rule_fiters.cy.ts new file mode 100644 index 0000000000000..262eab4ccd3d3 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rule_fiters.cy.ts @@ -0,0 +1,105 @@ +/* + * 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 { cleanKibana, resetRulesTableState, deleteAlertsAndRules } from '../../tasks/common'; +import { login, visitWithoutDateRange } from '../../tasks/login'; +import { esArchiverResetKibana } from '../../tasks/es_archiver'; +import { + expectRulesWithExecutionStatus, + filterByExecutionStatus, + expectNumberOfRulesShownOnPage, +} from '../../tasks/rule_filters'; + +import { SECURITY_DETECTIONS_RULES_URL } from '../../urls/navigation'; + +import { waitForRulesTableToBeLoaded } from '../../tasks/alerts_detection_rules'; + +import { createRule, waitForRulesToFinishExecution } from '../../tasks/api_calls/rules'; +import { deleteIndex, createIndex, createDocument } from '../../tasks/api_calls/elasticsearch'; + +import { getNewRule } from '../../objects/rule'; + +describe('Rule management filters', () => { + before(() => { + cleanKibana(); + }); + + beforeEach(() => { + login(); + // Make sure persisted rules table state is cleared + resetRulesTableState(); + deleteAlertsAndRules(); + esArchiverResetKibana(); + }); + + describe('Last response filter', () => { + it('Filters rules by last response', function () { + deleteIndex('test_index'); + + createIndex('test_index', { + '@timestamp': { + type: 'date', + }, + }); + + createDocument('test_index', {}); + + createRule( + getNewRule({ + name: 'Successful rule', + rule_id: 'successful_rule', + index: ['test_index'], + }) + ); + + createRule( + getNewRule({ + name: 'Warning rule', + rule_id: 'warning_rule', + index: ['non_existent_index'], + }) + ); + + createRule( + getNewRule({ + name: 'Failed rule', + rule_id: 'failed_rule', + index: ['test_index'], + // Setting a crazy large "Additional look-back time" to force a failure + from: 'now-9007199254746990s', + }) + ); + + waitForRulesToFinishExecution(['successful_rule', 'warning_rule', 'failed_rule'], new Date()); + + visitWithoutDateRange(SECURITY_DETECTIONS_RULES_URL); + + waitForRulesTableToBeLoaded(); + + // Initial table state - before filtering by status + expectNumberOfRulesShownOnPage(3); + expectRulesWithExecutionStatus(1, 'Succeeded'); + expectRulesWithExecutionStatus(1, 'Warning'); + expectRulesWithExecutionStatus(1, 'Failed'); + + // Table state after filtering by Succeeded status + filterByExecutionStatus('Succeeded'); + expectNumberOfRulesShownOnPage(1); + expectRulesWithExecutionStatus(1, 'Succeeded'); + + // Table state after filtering by Warning status + filterByExecutionStatus('Warning'); + expectNumberOfRulesShownOnPage(1); + expectRulesWithExecutionStatus(1, 'Warning'); + + // Table state after filtering by Failed status + filterByExecutionStatus('Failed'); + expectNumberOfRulesShownOnPage(1); + expectRulesWithExecutionStatus(1, 'Failed'); + }); + }); +}); 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 2a83a70338e5e..1431fa097b7ac 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 @@ -176,3 +176,9 @@ export const REFRESH_SETTINGS_SWITCH = '[data-test-subj="refreshSettingsSwitch"] export const REFRESH_SETTINGS_SELECTION_NOTE = '[data-test-subj="refreshSettingsSelectionNote"]'; export const REFRESH_RULES_STATUS = '[data-test-subj="refreshRulesStatus"]'; + +export const RULE_EXECUTION_STATUS_BADGE = '[data-test-subj="ruleExecutionStatus"]'; + +export const EXECUTION_STATUS_FILTER_BUTTON = '[data-test-subj="executionStatusFilterButton"]'; + +export const EXECUTION_STATUS_FILTER_OPTION = '[data-test-subj="executionStatusFilterOption"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/elasticsearch.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/elasticsearch.ts index b45e395822e82..84d8763c763a2 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/elasticsearch.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/elasticsearch.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { rootRequest } from '../common'; export const deleteIndex = (index: string) => { @@ -16,6 +15,24 @@ export const deleteIndex = (index: string) => { }); }; +export const createIndex = (indexName: string, properties: Record) => + rootRequest({ + method: 'PUT', + url: `${Cypress.env('ELASTICSEARCH_URL')}/${indexName}`, + body: { + mappings: { + properties, + }, + }, + }); + +export const createDocument = (indexName: string, document: Record) => + rootRequest({ + method: 'POST', + url: `${Cypress.env('ELASTICSEARCH_URL')}/${indexName}/_doc`, + body: document, + }); + export const waitForNewDocumentToBeIndexed = (index: string, initialNumberOfDocuments: number) => { cy.waitUntil( () => 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 5d5de9b7f146a..bd4e9122c34d7 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 @@ -7,9 +7,13 @@ import moment from 'moment'; import { rootRequest } from '../common'; -import { DETECTION_ENGINE_RULES_URL } from '../../../common/constants'; +import { + DETECTION_ENGINE_RULES_URL, + DETECTION_ENGINE_RULES_URL_FIND, +} from '../../../common/constants'; import type { RuleCreateProps, RuleResponse } from '../../../common/detection_engine/rule_schema'; import { internalAlertingSnoozeRule } from '../../urls/routes'; +import type { FetchRulesResponse } from '../../../public/detection_engine/rule_management/logic/types'; export const createRule = ( rule: RuleCreateProps @@ -72,3 +76,28 @@ export const importRule = (ndjsonPath: string) => { .should('be.equal', 200); }); }; + +export const waitForRulesToFinishExecution = (ruleIds: string[], afterDate?: Date) => + cy.waitUntil( + () => + rootRequest({ + method: 'GET', + url: DETECTION_ENGINE_RULES_URL_FIND, + }).then((response) => { + const areAllRulesFinished = ruleIds.every((ruleId) => + response.body.data.some((rule) => { + const ruleExecutionDate = rule.execution_summary?.last_execution?.date; + + const isDateOk = afterDate + ? !!(ruleExecutionDate && new Date(ruleExecutionDate) > afterDate) + : true; + + return ( + rule.rule_id === ruleId && typeof rule.execution_summary !== 'undefined' && isDateOk + ); + }) + ); + return areAllRulesFinished; + }), + { interval: 500, timeout: 12000 } + ); diff --git a/x-pack/plugins/security_solution/cypress/tasks/rule_filters.ts b/x-pack/plugins/security_solution/cypress/tasks/rule_filters.ts new file mode 100644 index 0000000000000..f917c29205d6d --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/tasks/rule_filters.ts @@ -0,0 +1,27 @@ +/* + * 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 { + RULE_EXECUTION_STATUS_BADGE, + EXECUTION_STATUS_FILTER_BUTTON, + EXECUTION_STATUS_FILTER_OPTION, +} from '../screens/alerts_detection_rules'; + +export const expectRulesWithExecutionStatus = (expectedCount: number, status: string) => { + cy.get(`${RULE_EXECUTION_STATUS_BADGE}:contains("${status}")`).should( + 'have.length', + expectedCount + ); +}; + +export const expectNumberOfRulesShownOnPage = (expectedCount: number) => + cy.get(RULE_EXECUTION_STATUS_BADGE).should('have.length', expectedCount); + +export const filterByExecutionStatus = (status: string) => { + cy.get(EXECUTION_STATUS_FILTER_BUTTON).click(); + cy.get(`${EXECUTION_STATUS_FILTER_OPTION}:contains("${status}")`).click(); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_filters/rule_execution_status_selector.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_filters/rule_execution_status_selector.tsx index 5840bcc79f9cd..b069b1b14ed49 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_filters/rule_execution_status_selector.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_filters/rule_execution_status_selector.tsx @@ -77,6 +77,7 @@ const RuleExecutionStatusSelectorComponent = ({ isSelected={isExecutionStatusPopoverOpen} hasActiveFilters={selectedStatus !== undefined} numActiveFilters={selectedStatus !== undefined ? 1 : 0} + data-test-subj="executionStatusFilterButton" > {i18n.COLUMN_LAST_RESPONSE} @@ -108,11 +109,13 @@ const RuleExecutionStatusSelectorComponent = ({ css={` margin-top: 4px; // aligns the badge within the option `} + data-test-subj="executionStatusFilterOption" > ); }} + data-test-subj="executionStatusFilterSelectableList" > {(list) => (