[Security Solution][Exceptions] - Updates to cypress tests and exceptions viewer (#140448)

## Summary

Following up on lingering suggestions from PR 138770 and working to unskip a flakey exceptions cypress test - skipped 
 in PR 140412.
This commit is contained in:
Yara Tercero 2022-09-19 17:57:16 -07:00 committed by GitHub
parent 6988f2a974
commit 656f75e896
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 94 additions and 61 deletions

1
.github/CODEOWNERS vendored
View file

@ -508,6 +508,7 @@ x-pack/examples/files_example @elastic/kibana-app-services
/x-pack/plugins/security_solution/cypress/e2e/exceptions @elastic/security-solution-platform
/x-pack/plugins/security_solution/cypress/e2e/value_lists @elastic/security-solution-platform
/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions @elastic/security-solution-platform
/x-pack/plugins/security_solution/public/common/components/exceptions @elastic/security-solution-platform
/x-pack/plugins/security_solution/public/exceptions @elastic/security-solution-platform
/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists @elastic/security-solution-platform

View file

@ -16,7 +16,7 @@ import { login, visitWithoutDateRange } from '../../tasks/login';
import {
openExceptionFlyoutFromEmptyViewerPrompt,
goToExceptionsTab,
editException,
openEditException,
} from '../../tasks/rule_details';
import {
addExceptionEntryFieldMatchAnyValue,
@ -278,7 +278,7 @@ describe('Exceptions flyout', () => {
});
describe('flyout errors', () => {
before(() => {
beforeEach(() => {
// create exception item via api
createExceptionListItem(getExceptionList().list_id, {
list_id: getExceptionList().list_id,
@ -303,9 +303,9 @@ describe('Exceptions flyout', () => {
goToExceptionsTab();
});
context.skip('When updating an item with version conflict', () => {
context('When updating an item with version conflict', () => {
it('Displays version conflict error', () => {
editException();
openEditException();
// update exception item via api
updateExceptionListItem('simple_list_item', {
@ -334,9 +334,9 @@ describe('Exceptions flyout', () => {
});
});
context.skip('When updating an item for a list that has since been deleted', () => {
context('When updating an item for a list that has since been deleted', () => {
it('Displays missing exception list error', () => {
editException();
openEditException();
// delete exception list via api
deleteExceptionList(getExceptionList().list_id, getExceptionList().namespace_type);

View file

@ -121,8 +121,8 @@ describe('Add exception from rule details', () => {
it('Creates an exception item', () => {
// displays existing exception items
cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('not.exist');
cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 2);
cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('not.exist');
// clicks prompt button to add a new exception item
addExceptionFromRuleDetails(getException());
@ -132,10 +132,10 @@ describe('Add exception from rule details', () => {
});
// Trying to figure out with EUI why the search won't trigger
it.skip('Can search for items', () => {
it('Can search for items', () => {
// displays existing exception items
cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('not.exist');
cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 2);
cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('not.exist');
// can search for an exception value
searchForExceptionItem('foo');

View file

@ -20,14 +20,13 @@ import { login, visitWithoutDateRange } from '../../../tasks/login';
import {
goToExceptionsTab,
waitForTheRuleToBeExecuted,
editException,
openEditException,
goToAlertsTab,
} from '../../../tasks/rule_details';
import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation';
import { deleteAlertsAndRules } from '../../../tasks/common';
import {
EXCEPTION_EDIT_FLYOUT_SAVE_BTN,
EXCEPTION_ITEM_VIEWER_CONTAINER,
EXCEPTION_ITEM_CONTAINER,
FIELD_INPUT,
@ -38,15 +37,14 @@ import {
deleteExceptionList,
} from '../../../tasks/api_calls/exceptions';
import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule';
import {
addExceptionEntryFieldValueOfItemX,
addExceptionEntryFieldValueValue,
} from '../../../tasks/exceptions';
import { editException } from '../../../tasks/exceptions';
import { ALERTS_COUNT, NUMBER_OF_ALERTS } from '../../../screens/alerts';
describe('Edit exception from rule details', () => {
const exceptionList = getExceptionList();
const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert';
const ITEM_FIELD = 'unique_value.test';
const FIELD_DIFFERENT_FROM_EXISTING_ITEM_FIELD = 'agent.name';
before(() => {
esArchiverResetKibana();
@ -90,7 +88,7 @@ describe('Edit exception from rule details', () => {
namespace_type: 'single',
entries: [
{
field: 'unique_value.test',
field: ITEM_FIELD,
operator: 'included',
type: 'match_any',
value: ['bar'],
@ -114,22 +112,13 @@ describe('Edit exception from rule details', () => {
// displays existing exception item
cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1);
editException();
openEditException();
// check that the existing item's field is being populated
cy.get(EXCEPTION_ITEM_CONTAINER)
.eq(0)
.find(FIELD_INPUT)
.eq(0)
.should('have.text', 'unique_value.test');
cy.get(EXCEPTION_ITEM_CONTAINER).eq(0).find(FIELD_INPUT).eq(0).should('have.text', ITEM_FIELD);
// check that you can select a different field
addExceptionEntryFieldValueOfItemX('agent.name{downarrow}{enter}', 0, 0);
addExceptionEntryFieldValueValue('foo', 0);
cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).click();
cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).should('have.attr', 'disabled');
cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).should('not.exist');
editException(FIELD_DIFFERENT_FROM_EXISTING_ITEM_FIELD, 0, 0);
cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1);
// Alerts table should still show single alert

View file

@ -20,14 +20,13 @@ import { login, visitWithoutDateRange } from '../../../tasks/login';
import {
goToExceptionsTab,
waitForTheRuleToBeExecuted,
editException,
openEditException,
goToAlertsTab,
} from '../../../tasks/rule_details';
import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation';
import { postDataView, deleteAlertsAndRules } from '../../../tasks/common';
import {
EXCEPTION_EDIT_FLYOUT_SAVE_BTN,
EXCEPTION_ITEM_VIEWER_CONTAINER,
EXCEPTION_ITEM_CONTAINER,
FIELD_INPUT,
@ -38,15 +37,14 @@ import {
deleteExceptionList,
} from '../../../tasks/api_calls/exceptions';
import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule';
import {
addExceptionEntryFieldValueOfItemX,
addExceptionEntryFieldValueValue,
} from '../../../tasks/exceptions';
import { editException } from '../../../tasks/exceptions';
import { ALERTS_COUNT, NUMBER_OF_ALERTS } from '../../../screens/alerts';
describe('Edit exception using data views from rule details', () => {
const exceptionList = getExceptionList();
const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert';
const ITEM_FIELD = 'unique_value.test';
const FIELD_DIFFERENT_FROM_EXISTING_ITEM_FIELD = 'agent.name';
before(() => {
esArchiverResetKibana();
@ -91,7 +89,7 @@ describe('Edit exception using data views from rule details', () => {
namespace_type: 'single',
entries: [
{
field: 'unique_value.test',
field: ITEM_FIELD,
operator: 'included',
type: 'match_any',
value: ['bar'],
@ -115,22 +113,13 @@ describe('Edit exception using data views from rule details', () => {
// displays existing exception item
cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1);
editException();
openEditException();
// check that the existing item's field is being populated
cy.get(EXCEPTION_ITEM_CONTAINER)
.eq(0)
.find(FIELD_INPUT)
.eq(0)
.should('have.text', 'unique_value.test');
cy.get(EXCEPTION_ITEM_CONTAINER).eq(0).find(FIELD_INPUT).eq(0).should('have.text', ITEM_FIELD);
// check that you can select a different field
addExceptionEntryFieldValueOfItemX('agent.name{downarrow}{enter}', 0, 0);
addExceptionEntryFieldValueValue('foo', 0);
cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).click();
cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).should('have.attr', 'disabled');
cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).should('not.exist');
editException(FIELD_DIFFERENT_FROM_EXISTING_ITEM_FIELD, 0, 0);
cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1);
// Alerts table should still show single alert

View file

@ -13,6 +13,7 @@ import {
EXCEPTION_FLYOUT_TITLE,
VALUES_INPUT,
VALUES_MATCH_ANY_INPUT,
EXCEPTION_EDIT_FLYOUT_SAVE_BTN,
} from '../screens/exceptions';
export const addExceptionEntryFieldValueOfItemX = (
@ -51,3 +52,12 @@ export const addExceptionEntryFieldMatchAnyValue = (value: string, index = 0) =>
export const closeExceptionBuilderFlyout = () => {
cy.get(CANCEL_BTN).click();
};
export const editException = (updatedField: string, itemIndex = 0, fieldIndex = 0) => {
addExceptionEntryFieldValueOfItemX(`${updatedField}{downarrow}{enter}`, itemIndex, fieldIndex);
addExceptionEntryFieldValueValue('foo', itemIndex);
cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).click();
cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).should('have.attr', 'disabled');
cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).should('not.exist');
};

View file

@ -129,10 +129,10 @@ export const goToExceptionsTab = () => {
cy.get(EXCEPTIONS_TAB).click();
};
export const editException = () => {
cy.get(EXCEPTION_ITEM_ACTIONS_BUTTON).eq(0).click({ force: true });
export const openEditException = (index = 0) => {
cy.get(EXCEPTION_ITEM_ACTIONS_BUTTON).eq(index).click({ force: true });
cy.get(EDIT_EXCEPTION_BTN).eq(0).click({ force: true });
cy.get(EDIT_EXCEPTION_BTN).eq(index).click({ force: true });
};
export const removeException = () => {

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useCallback, useMemo, useEffect, useReducer, useState } from 'react';
import React, { useCallback, useMemo, useEffect, useReducer } from 'react';
import { EuiPanel, EuiSpacer } from '@elastic/eui';
import type {
@ -57,6 +57,8 @@ const initialState: State = {
exceptionToEdit: null,
currenFlyout: null,
viewerState: 'loading',
isReadOnly: true,
lastUpdated: Date.now(),
};
export interface GetExceptionItemProps {
@ -79,8 +81,6 @@ const ExceptionsViewerComponent = ({
const { services } = useKibana();
const toasts = useToasts();
const [{ canUserCRUD, hasIndexWrite }] = useUserData();
const [isReadOnly, setReadOnly] = useState(true);
const [lastUpdated, setLastUpdated] = useState<null | string | number>(null);
const exceptionListsToQuery = useMemo(
() =>
rule != null && rule.exceptions_list != null
@ -90,12 +90,24 @@ const ExceptionsViewerComponent = ({
);
// Reducer state
const [{ exceptions, pagination, currenFlyout, exceptionToEdit, viewerState }, dispatch] =
useReducer(allExceptionItemsReducer(), {
...initialState,
});
const [
{ exceptions, pagination, currenFlyout, exceptionToEdit, viewerState, isReadOnly, lastUpdated },
dispatch,
] = useReducer(allExceptionItemsReducer(), {
...initialState,
});
// Reducer actions
const setLastUpdated = useCallback(
(lastUpdate: string | number): void => {
dispatch({
type: 'setLastUpdateTime',
lastUpdate,
});
},
[dispatch]
);
const setExceptions = useCallback(
({
exceptions: newExceptions,
@ -109,7 +121,7 @@ const ExceptionsViewerComponent = ({
pagination: newPagination,
});
},
[dispatch]
[dispatch, setLastUpdated]
);
const setViewerState = useCallback(
@ -132,6 +144,16 @@ const ExceptionsViewerComponent = ({
[dispatch]
);
const setReadOnly = useCallback(
(readOnly: boolean): void => {
dispatch({
type: 'setIsReadOnly',
readOnly,
});
},
[dispatch]
);
const [_, allReferences] = useFindExceptionListReferences(exceptionListsToQuery);
const handleFetchItems = useCallback(
@ -173,7 +195,7 @@ const ExceptionsViewerComponent = ({
signal: abortCtrl.signal,
});
// Please see `x-pack/plugins/lists/public/exceptions/transforms.ts` doc notes
// Please see `kbn-securitysolution-list-hooks/src/transforms/index.test.ts` doc notes
// for context around the temporary `id`
const transformedData = data.map((item) => transformInput(item));

View file

@ -27,6 +27,8 @@ export interface State {
// Flyout to be opened (edit vs add vs none)
currenFlyout: ViewerFlyoutName;
viewerState: ViewerState;
isReadOnly: boolean;
lastUpdated: string | number;
}
export type Action =
@ -43,6 +45,14 @@ export type Action =
| {
type: 'setViewerState';
state: ViewerState;
}
| {
type: 'setIsReadOnly';
readOnly: boolean;
}
| {
type: 'setLastUpdateTime';
lastUpdate: string | number;
};
export const allExceptionItemsReducer =
@ -82,6 +92,18 @@ export const allExceptionItemsReducer =
viewerState: action.state,
};
}
case 'setIsReadOnly': {
return {
...state,
isReadOnly: action.readOnly,
};
}
case 'setLastUpdateTime': {
return {
...state,
lastUpdated: action.lastUpdate,
};
}
default:
return state;
}

View file

@ -85,7 +85,7 @@ const ExceptionsViewerSearchBarComponent = ({
<EuiSearchBar
box={{
placeholder: 'Search on the fields below: e.g. name:"my list"',
incremental: false,
incremental: true,
schema: ITEMS_SCHEMA,
'data-test-subj': 'exceptionsViewerSearchBar',
}}

View file

@ -35,7 +35,7 @@ const StyledCondition = styled.span`
interface ExceptionsViewerUtilityProps {
pagination: ExceptionsPagination;
// Corresponds to last time exception items were fetched
lastUpdated: string | number | null;
lastUpdated: string | number;
}
/**