[Security Solution] [Exceptions] Fix Exception Auto-populate from Alert actions (#159908)

## Summary

- Addresses  https://github.com/elastic/kibana/issues/159784

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Wafaa Nasr 2023-06-28 12:14:19 +01:00 committed by GitHub
parent 5c0db11260
commit fdd709b025
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 237 additions and 36 deletions

View file

@ -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<boolean>(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 (
<EuiFlexGroup gutterSize="s" direction="column" data-test-subj="exceptionsBuilderWrapper">

View file

@ -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);
});
});

View file

@ -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,7 @@ 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
@ -154,10 +169,112 @@ 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);
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
* 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);
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();
});
});

View file

@ -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();

View file

@ -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));
};

View file

@ -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}.',
}
);

View file

@ -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 (
<div>
<CommentAccordion
initialIsOpen={initialIsOpen}
initialIsOpen={initialIsOpen && !!newCommentValue}
id={'add-exception-comments-accordion'}
buttonClassName={COMMENT_ACCORDION_BUTTON_CLASS_NAME}
buttonContent={accordionTitle ?? commentsAccordionTitle}

View file

@ -419,6 +419,7 @@ export const EventFiltersForm: React.FC<ArtifactFormComponentProps & { allowSele
comments: exception?.comments ?? [],
os_types: exception?.os_types ?? [OperatingSystem.WINDOWS],
tags: exception?.tags ?? [],
meta: exception.meta,
}
: exception;
const hasValidConditions =