diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx index 310c10aaa1581..9af52e3c724c7 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useEffect, useMemo, useReducer } from 'react'; +import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import styled from 'styled-components'; import { HttpStart } from '@kbn/core/public'; @@ -34,6 +34,7 @@ import { } from '@kbn/securitysolution-list-utils'; import { DataViewBase } from '@kbn/es-query'; import type { AutocompleteStart } from '@kbn/unified-search-plugin/public'; +import deepEqual from 'fast-deep-equal'; import { AndOrBadge } from '../and_or_badge'; @@ -41,7 +42,6 @@ import { BuilderExceptionListItemComponent } from './exception_item_renderer'; import { BuilderLogicButtons } from './logic_buttons'; import { getTotalErrorExist } from './selectors'; import { EntryFieldError, State, exceptionsBuilderReducer } from './reducer'; - const MyInvisibleAndBadge = styled(EuiFlexItem)` visibility: hidden; `; @@ -131,6 +131,7 @@ export const ExceptionBuilderComponent = ({ disableNested: isNestedDisabled, disableOr: isOrDisabled, }); + const [areAllEntriesDeleted, setAreAllEntriesDeleted] = useState(false); const { addNested, @@ -252,6 +253,7 @@ export const ExceptionBuilderComponent = ({ // just add a default entry to it if (updatedExceptions.length === 0) { setDefaultExceptions(item); + setAreAllEntriesDeleted(true); } else if (updatedExceptions.length > 0 && exceptionListItemSchema.is(item)) { setUpdateExceptionsToDelete([...exceptionsToDelete, item]); } else { @@ -394,12 +396,36 @@ export const ExceptionBuilderComponent = ({ } }, [exceptions, handleAddNewExceptionItem]); + /** + * This component relies on the "exceptionListItems" to pre-fill its entries, + * but any subsequent updates to the entries are not reflected back to + * the "exceptionListItems". To ensure correct behavior, we need to only + * fill the entries from the "exceptionListItems" during initialization. + * + * In the initialization phase, if there are "exceptionListItems" with + * pre-filled entries, the exceptions array will be empty. However, + * there are cases where the "exceptionListItems" may not be sent + * correctly during initialization, leading to the exceptions + * array being filled with empty entries. Therefore, we need to + * check if the exception is correctly populated with a valid + * "field" when the "exceptionListItems" has entries. that's why + * "exceptionsEntriesPopulated" is used + * + * It's important to differentiate this case from when the user + * deletes all the entries and the "exceptionListItems" has pre-filled values. + * that's why "allEntriesDeleted" is used + * + * deepEqual(exceptionListItems, exceptions) to handle the exceptionListItems in + * the EventFiltersFlyout + */ useEffect(() => { - if (exceptionListItems.length > 0) { + if (!exceptionListItems.length || deepEqual(exceptionListItems, exceptions)) return; + const exceptionsEntriesPopulated = exceptions.some((exception) => + exception.entries.some((entry) => entry.field) + ); + if (!exceptionsEntriesPopulated && !areAllEntriesDeleted) setUpdateExceptions(exceptionListItems); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [areAllEntriesDeleted, exceptionListItems, exceptions, setUpdateExceptions]); return ( diff --git a/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/endpoint_exceptions.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/endpoint_exceptions.cy.ts index 7497316927ee3..f333376b40231 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/endpoint_exceptions.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/endpoint_exceptions.cy.ts @@ -7,8 +7,10 @@ import { deleteAlertsAndRules } from '../../../tasks/common'; import { + expandFirstAlert, goToClosedAlertsOnRuleDetailsPage, goToOpenedAlertsOnRuleDetailsPage, + openAddEndpointExceptionFromAlertActionButton, openAddEndpointExceptionFromFirstAlert, } from '../../../tasks/alerts'; import { login, visitWithoutDateRange } from '../../../tasks/login'; @@ -26,13 +28,22 @@ import { } from '../../../tasks/es_archiver'; import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation'; import { + addExceptionEntryFieldValue, + addExceptionEntryFieldValueValue, addExceptionFlyoutItemName, + editExceptionFlyoutItemName, selectCloseSingleAlerts, submitNewExceptionItem, validateExceptionConditionField, } from '../../../tasks/exceptions'; import { ALERTS_COUNT, EMPTY_ALERT_TABLE } from '../../../screens/alerts'; -import { NO_EXCEPTIONS_EXIST_PROMPT } from '../../../screens/exceptions'; +import { + ADD_AND_BTN, + EXCEPTION_CARD_ITEM_CONDITIONS, + EXCEPTION_CARD_ITEM_NAME, + EXCEPTION_ITEM_VIEWER_CONTAINER, + NO_EXCEPTIONS_EXIST_PROMPT, +} from '../../../screens/exceptions'; import { removeException, goToAlertsTab, @@ -41,10 +52,11 @@ import { describe('Endpoint Exceptions workflows from Alert', () => { const expectedNumberOfAlerts = 1; - before(() => { - esArchiverResetKibana(); - }); + const ITEM_NAME = 'Sample Exception List Item'; + const ITEM_NAME_EDIT = 'Sample Exception List Item'; + const ADDITIONAL_ENTRY = 'host.hostname'; beforeEach(() => { + esArchiverResetKibana(); login(); deleteAlertsAndRules(); esArchiverLoad('endpoint'); @@ -69,7 +81,7 @@ describe('Endpoint Exceptions workflows from Alert', () => { validateExceptionConditionField('file.Ext.code_signature'); selectCloseSingleAlerts(); - addExceptionFlyoutItemName('Sample Exception'); + addExceptionFlyoutItemName(ITEM_NAME); submitNewExceptionItem(); // Alerts table should now be empty from having added exception and closed @@ -100,4 +112,39 @@ describe('Endpoint Exceptions workflows from Alert', () => { cy.get(ALERTS_COUNT).should('have.text', `${expectedNumberOfAlerts} alert`); }); + + it('Should be able to create Endpoint exception from Alerts take action button, and change multiple exception items without resetting to initial auto-prefilled entries', () => { + // Open first Alert Summary + expandFirstAlert(); + + // The Endpoint should populated with predefined fields + openAddEndpointExceptionFromAlertActionButton(); + + // As the endpoint.alerts-* is used to trigger the alert the + // file.Ext.code_signature will be auto-populated + validateExceptionConditionField('file.Ext.code_signature'); + addExceptionFlyoutItemName(ITEM_NAME); + + cy.get(ADD_AND_BTN).click(); + // edit conditions + addExceptionEntryFieldValue(ADDITIONAL_ENTRY, 6); + addExceptionEntryFieldValueValue('foo', 4); + + // Change the name again + editExceptionFlyoutItemName(ITEM_NAME_EDIT); + + // validate the condition is still "agent.name" or got rest after the name is changed + validateExceptionConditionField(ADDITIONAL_ENTRY); + + selectCloseSingleAlerts(); + submitNewExceptionItem(); + + // Endpoint Exception will move to Endpoint List under Exception tab of rule + goToEndpointExceptionsTab(); + + // new exception item displays + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + cy.get(EXCEPTION_CARD_ITEM_NAME).should('have.text', ITEM_NAME_EDIT); + cy.get(EXCEPTION_CARD_ITEM_CONDITIONS).contains('span', ADDITIONAL_ENTRY); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/rule_exceptions.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/rule_exceptions.cy.ts index 935da49546b49..59af4592d2ebe 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/rule_exceptions.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/rule_exceptions.cy.ts @@ -12,8 +12,10 @@ import { createRule } from '../../../tasks/api_calls/rules'; import { goToRuleDetails } from '../../../tasks/alerts_detection_rules'; import { addExceptionFromFirstAlert, + expandFirstAlert, goToClosedAlertsOnRuleDetailsPage, goToOpenedAlertsOnRuleDetailsPage, + openAddRuleExceptionFromAlertActionButton, } from '../../../tasks/alerts'; import { addExceptionEntryFieldValue, @@ -26,6 +28,9 @@ import { validateExceptionItemAffectsTheCorrectRulesInRulePage, validateExceptionConditionField, validateExceptionCommentCountAndText, + editExceptionFlyoutItemName, + validateHighlightedFieldsPopulatedAsExceptionConditions, + validateEmptyExceptionConditionField, } from '../../../tasks/exceptions'; import { esArchiverLoad, @@ -42,26 +47,44 @@ import { import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation'; import { postDataView, deleteAlertsAndRules } from '../../../tasks/common'; -import { NO_EXCEPTIONS_EXIST_PROMPT } from '../../../screens/exceptions'; +import { + ADD_AND_BTN, + ENTRY_DELETE_BTN, + EXCEPTION_CARD_ITEM_CONDITIONS, + EXCEPTION_CARD_ITEM_NAME, + EXCEPTION_ITEM_VIEWER_CONTAINER, + NO_EXCEPTIONS_EXIST_PROMPT, +} from '../../../screens/exceptions'; import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule'; +const loadEndpointRuleAndAlerts = () => { + esArchiverLoad('endpoint'); + login(); + createRule(getEndpointRule()); + visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); + goToRuleDetails(); + waitForAlertsToPopulate(); +}; + describe('Rule Exceptions workflows from Alert', () => { const EXPECTED_NUMBER_OF_ALERTS = '1 alert'; - const ITEM_NAME = 'Sample Exception List Item'; + const ITEM_NAME = 'Sample Exception Item'; + const ITEM_NAME_EDIT = 'Sample Exception Item Edit'; + const ADDITIONAL_ENTRY = 'host.hostname'; const newRule = getNewRule(); beforeEach(() => { esArchiverResetKibana(); - deleteAlertsAndRules(); }); after(() => { esArchiverUnload('exceptions'); + deleteAlertsAndRules(); }); afterEach(() => { esArchiverUnload('exceptions_2'); }); - it('Creates an exception item from alert actions overflow menu and close all matching alerts', () => { + it('Should create a Rule exception item from alert actions overflow menu and close all matching alerts', () => { esArchiverLoad('exceptions'); login(); postDataView('exceptions-*'); @@ -119,14 +142,8 @@ describe('Rule Exceptions workflows from Alert', () => { cy.get(ALERTS_COUNT).should('have.text', '2 alerts'); }); - - it('Creates an exception item from alert actions overflow menu and auto populate the conditions using alert Highlighted fields ', () => { - esArchiverLoad('endpoint'); - login(); - createRule(getEndpointRule()); - visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); - goToRuleDetails(); - waitForAlertsToPopulate(); + it('Should create a Rule exception item from alert actions overflow menu and auto populate the conditions using alert Highlighted fields', () => { + loadEndpointRuleAndAlerts(); cy.get(LOADING_INDICATOR).should('not.exist'); addExceptionFromFirstAlert(); @@ -144,9 +161,45 @@ describe('Rule Exceptions workflows from Alert', () => { * fields are based on the alert document that should be generated * when the endpoint rule runs */ - highlightedFieldsBasedOnAlertDoc.forEach((field, index) => { - validateExceptionConditionField(field); - }); + validateHighlightedFieldsPopulatedAsExceptionConditions(highlightedFieldsBasedOnAlertDoc); + + /** + * Validate that the comments are opened by default with one comment added + * showing a text contains information about the pre-filled conditions + */ + validateExceptionCommentCountAndText( + 1, + 'Exception conditions are pre-filled with relevant data from alert with "id"' + ); + + addExceptionFlyoutItemName(ITEM_NAME); + submitNewExceptionItem(); + }); + it('Should create a Rule exception from Alerts take action button and change multiple exception items without resetting to initial auto-prefilled entries', () => { + loadEndpointRuleAndAlerts(); + + cy.get(LOADING_INDICATOR).should('not.exist'); + + // Open first Alert Summary + expandFirstAlert(); + + // The Rule exception should populated with highlighted fields + openAddRuleExceptionFromAlertActionButton(); + + const highlightedFieldsBasedOnAlertDoc = [ + 'host.name', + 'agent.id', + 'user.name', + 'process.executable', + 'file.path', + ]; + + /** + * Validate the highlighted fields are auto populated, these + * fields are based on the alert document that should be generated + * when the endpoint rule runs + */ + validateHighlightedFieldsPopulatedAsExceptionConditions(highlightedFieldsBasedOnAlertDoc); /** * Validate that the comments are opened by default with one comment added @@ -154,10 +207,74 @@ describe('Rule Exceptions workflows from Alert', () => { */ validateExceptionCommentCountAndText( 1, - 'Exception conditions are pre-filled with relevant data from' + 'Exception conditions are pre-filled with relevant data from alert with "id"' ); addExceptionFlyoutItemName(ITEM_NAME); + + cy.get(ADD_AND_BTN).click(); + + // edit conditions + addExceptionEntryFieldValue(ADDITIONAL_ENTRY, 5); + addExceptionEntryFieldValueValue('foo', 5); + + // Change the name again + editExceptionFlyoutItemName(ITEM_NAME_EDIT); + + // validate the condition is still 'host.hostname' or got rest after the name is changed + validateExceptionConditionField(ADDITIONAL_ENTRY); + submitNewExceptionItem(); + + goToExceptionsTab(); + + // new exception item displays + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + cy.get(EXCEPTION_CARD_ITEM_NAME).should('have.text', ITEM_NAME_EDIT); + cy.get(EXCEPTION_CARD_ITEM_CONDITIONS).contains('span', 'host.hostname'); + }); + it('Should delete all prefilled exception entries when creating a Rule exception from Alerts take action button without resetting to initial auto-prefilled entries', () => { + loadEndpointRuleAndAlerts(); + + cy.get(LOADING_INDICATOR).should('not.exist'); + + // Open first Alert Summary + expandFirstAlert(); + + // The Rule exception should populated with highlighted fields + openAddRuleExceptionFromAlertActionButton(); + + const highlightedFieldsBasedOnAlertDoc = [ + 'host.name', + 'agent.id', + 'user.name', + 'process.executable', + 'file.path', + ]; + + /** + * Validate the highlighted fields are auto populated, these + * fields are based on the alert document that should be generated + * when the endpoint rule runs + */ + validateHighlightedFieldsPopulatedAsExceptionConditions(highlightedFieldsBasedOnAlertDoc); + + /** + * Delete all the highlighted fields to see if any condition + * will prefuilled again. + */ + const highlightedFieldsCount = highlightedFieldsBasedOnAlertDoc.length - 1; + highlightedFieldsBasedOnAlertDoc.forEach((_, index) => + cy + .get(ENTRY_DELETE_BTN) + .eq(highlightedFieldsCount - index) + .click() + ); + + /** + * Validate that there are no highlighted fields are auto populated + * after the deletion + */ + validateEmptyExceptionConditionField(); }); }); diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts index c5d2aebee7c54..67d61925fa164 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts @@ -92,16 +92,18 @@ export const openAddEndpointExceptionFromFirstAlert = () => { cy.get(FIELD_INPUT).should('be.visible'); }; -export const openAddExceptionFromAlertDetails = () => { - cy.get(EXPAND_ALERT_BTN).first().click({ force: true }); - +export const openAddRuleExceptionFromAlertActionButton = () => { cy.get(TAKE_ACTION_BTN).click(); cy.get(TAKE_ACTION_MENU).should('be.visible'); - cy.get(ADD_EXCEPTION_BTN).click(); - cy.get(ADD_EXCEPTION_BTN).should('not.be.visible'); + cy.get(ADD_EXCEPTION_BTN, { timeout: 10000 }).first().click(); }; +export const openAddEndpointExceptionFromAlertActionButton = () => { + cy.get(TAKE_ACTION_BTN).click(); + cy.get(TAKE_ACTION_MENU).should('be.visible'); + cy.get(ADD_ENDPOINT_EXCEPTION_BTN, { timeout: 10000 }).first().click(); +}; export const closeFirstAlert = () => { expandFirstAlertActions(); cy.get(CLOSE_ALERT_BTN).click(); diff --git a/x-pack/plugins/security_solution/cypress/tasks/exceptions.ts b/x-pack/plugins/security_solution/cypress/tasks/exceptions.ts index 884c4521985fc..9195153d46f6f 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/exceptions.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/exceptions.ts @@ -167,6 +167,9 @@ export const addExceptionConditions = (exception: Exception) => { export const validateExceptionConditionField = (value: string) => { cy.get(EXCEPTION_ITEM_CONTAINER).contains('span', value); }; +export const validateEmptyExceptionConditionField = () => { + cy.get(FIELD_INPUT).should('be.empty'); +}; export const submitNewExceptionItem = () => { cy.get(CONFIRM_BTN).click(); cy.get(CONFIRM_BTN).should('not.exist'); @@ -279,3 +282,8 @@ export const deleteFirstExceptionItemInListDetailPage = () => { // Delete exception cy.get(EXCEPTION_ITEM_OVERFLOW_ACTION_DELETE).click(); }; +export const validateHighlightedFieldsPopulatedAsExceptionConditions = ( + highlightedFields: string[] +) => { + return highlightedFields.every((field) => validateExceptionConditionField(field)); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/translations.ts index 4b4ad1dac1b5e..215bb3fc29923 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/translations.ts @@ -89,6 +89,7 @@ export const ADD_RULE_EXCEPTION_FROM_ALERT_COMMENT = (alertId: string) => 'xpack.securitySolution.ruleExceptions.addExceptionFlyout.addRuleExceptionFromAlertComment', { values: { alertId }, - defaultMessage: 'Exception conditions are pre-filled with relevant data from {alertId}.', + defaultMessage: + 'Exception conditions are pre-filled with relevant data from alert with "id" {alertId}.', } ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/index.tsx index 38ece2d1ba43c..0f32e2b4d1ab8 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/index.tsx @@ -96,7 +96,7 @@ export const ExceptionItemComments = memo(function ExceptionItemComments({ } else { return null; } - }, [shouldShowComments, exceptionItemComments]); + }, [exceptionItemComments, shouldShowComments]); const formattedComments = useMemo((): EuiCommentProps[] => { if (exceptionItemComments && exceptionItemComments.length > 0) { @@ -105,11 +105,10 @@ export const ExceptionItemComments = memo(function ExceptionItemComments({ return []; } }, [exceptionItemComments]); - return (