Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Jun 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -34,14 +34,14 @@ 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';

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;
`;
Expand Down Expand Up @@ -131,6 +131,7 @@ export const ExceptionBuilderComponent = ({
disableNested: isNestedDisabled,
disableOr: isOrDisabled,
});
const [areAllEntriesDeleted, setAreAllEntriesDeleted] = useState<boolean>(false);

const {
addNested,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@

import { deleteAlertsAndRules } from '../../../tasks/common';
import {
expandFirstAlert,
goToClosedAlertsOnRuleDetailsPage,
goToOpenedAlertsOnRuleDetailsPage,
openAddEndpointExceptionFromAlertActionButton,
openAddEndpointExceptionFromFirstAlert,
} from '../../../tasks/alerts';
import { login, visitWithoutDateRange } from '../../../tasks/login';
Expand All @@ -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,
Expand All @@ -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');
Expand All @@ -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
Expand Down Expand Up @@ -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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -26,6 +28,9 @@ import {
validateExceptionItemAffectsTheCorrectRulesInRulePage,
validateExceptionConditionField,
validateExceptionCommentCountAndText,
editExceptionFlyoutItemName,
validateHighlightedFieldsPopulatedAsExceptionConditions,
validateEmptyExceptionConditionField,
} from '../../../tasks/exceptions';
import {
esArchiverLoad,
Expand All @@ -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-*');
Expand Down Expand Up @@ -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();
Expand All @@ -144,20 +161,120 @@ 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
* showing a text contains information about the pre-filled conditions
*/
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();
});
});
Loading