diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts index d68def1284468..60202a4f6a52a 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts @@ -29,10 +29,21 @@ import { EXCEPTION_ITEM_CONTAINER, ADD_EXCEPTIONS_BTN, EXCEPTION_FIELD_LIST, + EDIT_EXCEPTIONS_BTN, + EXCEPTION_EDIT_FLYOUT_SAVE_BTN, + EXCEPTION_FLYOUT_VERSION_CONFLICT, + EXCEPTION_FLYOUT_LIST_DELETED_ERROR, } from '../../screens/exceptions'; import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation'; import { cleanKibana, reload } from '../../tasks/common'; +import { + createExceptionList, + createExceptionListItem, + updateExceptionListItem, + deleteExceptionList, +} from '../../tasks/api_calls/exceptions'; +import { getExceptionList } from '../../objects/exception'; // NOTE: You might look at these tests and feel they're overkill, // but the exceptions flyout has a lot of logic making it difficult @@ -42,18 +53,28 @@ import { cleanKibana, reload } from '../../tasks/common'; describe('Exceptions flyout', () => { before(() => { cleanKibana(); - loginAndWaitForPageWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); - createCustomRule({ ...getNewRule(), index: ['exceptions-*'] }); - reload(); - goToRuleDetails(); - - cy.get(RULE_STATUS).should('have.text', '—'); - // this is a made-up index that has just the necessary // mappings to conduct tests, avoiding loading large // amounts of data like in auditbeat_exceptions esArchiverLoad('exceptions'); - + loginAndWaitForPageWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); + createExceptionList(getExceptionList(), getExceptionList().list_id).then((response) => + createCustomRule({ + ...getNewRule(), + index: ['exceptions-*'], + exceptionLists: [ + { + id: response.body.id, + list_id: getExceptionList().list_id, + type: getExceptionList().type, + namespace_type: getExceptionList().namespace_type, + }, + ], + }) + ); + reload(); + goToRuleDetails(); + cy.get(RULE_STATUS).should('have.text', '—'); goToExceptionsTab(); }); @@ -62,7 +83,12 @@ describe('Exceptions flyout', () => { }); it('Does not overwrite values and-ed together', () => { - cy.get(ADD_EXCEPTIONS_BTN).click({ force: true }); + cy.root() + .pipe(($el) => { + $el.find(ADD_EXCEPTIONS_BTN).trigger('click'); + return $el.find(ADD_AND_BTN); + }) + .should('be.visible'); // add multiple entries with invalid field values addExceptionEntryFieldValue('agent.name', 0); @@ -80,8 +106,12 @@ describe('Exceptions flyout', () => { }); it('Does not overwrite values or-ed together', () => { - cy.get(ADD_EXCEPTIONS_BTN).click({ force: true }); - + cy.root() + .pipe(($el) => { + $el.find(ADD_EXCEPTIONS_BTN).trigger('click'); + return $el.find(ADD_AND_BTN); + }) + .should('be.visible'); // exception item 1 addExceptionEntryFieldValueOfItemX('agent.name', 0, 0); cy.get(ADD_AND_BTN).click(); @@ -197,11 +227,89 @@ describe('Exceptions flyout', () => { }); it('Contains custom index fields', () => { - cy.get(ADD_EXCEPTIONS_BTN).click({ force: true }); - + cy.root() + .pipe(($el) => { + $el.find(ADD_EXCEPTIONS_BTN).trigger('click'); + return $el.find(ADD_AND_BTN); + }) + .should('be.visible'); cy.get(FIELD_INPUT).eq(0).click({ force: true }); cy.get(EXCEPTION_FIELD_LIST).contains('unique_value.test'); closeExceptionBuilderFlyout(); }); + + describe('flyout errors', () => { + before(() => { + // create exception item via api + createExceptionListItem(getExceptionList().list_id, { + list_id: getExceptionList().list_id, + item_id: 'simple_list_item', + tags: [], + type: 'simple', + description: 'Test exception item', + name: 'Sample Exception List Item', + namespace_type: 'single', + entries: [ + { + field: 'host.name', + operator: 'included', + type: 'match_any', + value: ['some host', 'another host'], + }, + ], + }); + + reload(); + cy.get(RULE_STATUS).should('have.text', '—'); + goToExceptionsTab(); + }); + + context('When updating an item with version conflict', () => { + it('Displays version conflict error', () => { + cy.get(EDIT_EXCEPTIONS_BTN).should('be.visible'); + cy.get(EDIT_EXCEPTIONS_BTN).click({ force: true }); + + // update exception item via api + updateExceptionListItem('simple_list_item', { + name: 'Updated item name', + item_id: 'simple_list_item', + tags: [], + type: 'simple', + description: 'Test exception item', + namespace_type: 'single', + entries: [ + { + field: 'host.name', + operator: 'included', + type: 'match_any', + value: ['some host', 'another host'], + }, + ], + }); + + // try to save and see version conflict error + cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).click({ force: true }); + + cy.get(EXCEPTION_FLYOUT_VERSION_CONFLICT).should('be.visible'); + + closeExceptionBuilderFlyout(); + }); + }); + + context('When updating an item for a list that has since been deleted', () => { + it('Displays missing exception list error', () => { + cy.get(EDIT_EXCEPTIONS_BTN).should('be.visible'); + cy.get(EDIT_EXCEPTIONS_BTN).click({ force: true }); + + // delete exception list via api + deleteExceptionList(getExceptionList().list_id, getExceptionList().namespace_type); + + // try to save and see error + cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).click({ force: true }); + + cy.get(EXCEPTION_FLYOUT_LIST_DELETED_ERROR).should('be.visible'); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/objects/exception.ts b/x-pack/plugins/security_solution/cypress/objects/exception.ts index fbb000f43fdd2..637adc9fc0134 100644 --- a/x-pack/plugins/security_solution/cypress/objects/exception.ts +++ b/x-pack/plugins/security_solution/cypress/objects/exception.ts @@ -22,6 +22,17 @@ export interface ExceptionList { type: 'detection' | 'endpoint'; } +export interface ExceptionListItem { + description: string; + list_id: string; + item_id: string; + name: string; + namespace_type: 'single' | 'agnostic'; + tags: string[]; + type: 'simple'; + entries: Array<{ field: string; operator: string; type: string; value: string[] }>; +} + export const getExceptionList = (): ExceptionList => ({ description: 'Test exception list description', list_id: 'test_exception_list', diff --git a/x-pack/plugins/security_solution/cypress/screens/exceptions.ts b/x-pack/plugins/security_solution/cypress/screens/exceptions.ts index e1b9e0639dfaa..c94c2be8b976f 100644 --- a/x-pack/plugins/security_solution/cypress/screens/exceptions.ts +++ b/x-pack/plugins/security_solution/cypress/screens/exceptions.ts @@ -5,6 +5,8 @@ * 2.0. */ +export const EDIT_EXCEPTIONS_BTN = '[data-test-subj="exceptionsViewerEditBtn"]'; + export const ADD_EXCEPTIONS_BTN = '[data-test-subj="exceptionsHeaderAddExceptionBtn"]'; export const CLOSE_ALERTS_CHECKBOX = @@ -61,3 +63,10 @@ export const EXCEPTION_FIELD_LIST = '[data-test-subj="comboBoxOptionsList fieldAutocompleteComboBox-optionsList"]'; export const EXCEPTION_FLYOUT_TITLE = '[data-test-subj="exception-flyout-title"]'; + +export const EXCEPTION_EDIT_FLYOUT_SAVE_BTN = '[data-test-subj="edit-exception-confirm-button"]'; + +export const EXCEPTION_FLYOUT_VERSION_CONFLICT = + '[data-test-subj="exceptionsFlyoutVersionConflict"]'; + +export const EXCEPTION_FLYOUT_LIST_DELETED_ERROR = '[data-test-subj="errorCalloutContainer"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index a057f27df4280..a9134e5b124eb 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -33,6 +33,8 @@ export const DETAILS_TITLE = '.euiDescriptionList__title'; export const EXCEPTIONS_TAB = '[data-test-subj="exceptionsTab"]'; +export const EXCEPTIONS_TAB_SEARCH = '[data-test-subj="exceptionsHeaderSearch"]'; + export const FALSE_POSITIVES_DETAILS = 'False positive examples'; export const INDEX_PATTERNS_DETAILS = 'Index patterns'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/exceptions.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/exceptions.ts index 7363bd5991b1c..ab6c649c7c61f 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/exceptions.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/exceptions.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ExceptionList } from '../../objects/exception'; +import { ExceptionList, ExceptionListItem } from '../../objects/exception'; export const createExceptionList = ( exceptionList: ExceptionList, @@ -23,3 +23,58 @@ export const createExceptionList = ( headers: { 'kbn-xsrf': 'cypress-creds' }, failOnStatusCode: false, }); + +export const createExceptionListItem = ( + exceptionListId: string, + exceptionListItem?: ExceptionListItem +) => + cy.request({ + method: 'POST', + url: '/api/exception_lists/items', + body: { + list_id: exceptionListItem?.list_id ?? exceptionListId, + item_id: exceptionListItem?.item_id ?? 'simple_list_item', + tags: exceptionListItem?.tags ?? ['user added string for a tag', 'malware'], + type: exceptionListItem?.type ?? 'simple', + description: exceptionListItem?.description ?? 'This is a sample endpoint type exception', + name: exceptionListItem?.name ?? 'Sample Exception List Item', + entries: exceptionListItem?.entries ?? [ + { + field: 'actingProcess.file.signer', + operator: 'excluded', + type: 'exists', + }, + { + field: 'host.name', + operator: 'included', + type: 'match_any', + value: ['some host', 'another host'], + }, + ], + }, + headers: { 'kbn-xsrf': 'cypress-creds' }, + failOnStatusCode: false, + }); + +export const updateExceptionListItem = ( + exceptionListItemId: string, + exceptionListItemUpdate?: Partial +) => + cy.request({ + method: 'PUT', + url: '/api/exception_lists/items', + body: { + item_id: exceptionListItemId, + ...exceptionListItemUpdate, + }, + headers: { 'kbn-xsrf': 'cypress-creds' }, + failOnStatusCode: false, + }); + +export const deleteExceptionList = (listId: string, namespaceType: string) => + cy.request({ + method: 'DELETE', + url: `/api/exception_lists?list_id=${listId}&namespace_type=${namespaceType}`, + headers: { 'kbn-xsrf': 'cypress-creds' }, + failOnStatusCode: false, + }); diff --git a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts index e69c217c4a764..d42ebcf9da68e 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts @@ -91,7 +91,12 @@ export const goToAlertsTab = () => { }; export const goToExceptionsTab = () => { - cy.get(EXCEPTIONS_TAB).click(); + cy.root() + .pipe(($el) => { + $el.find(EXCEPTIONS_TAB).trigger('click'); + return $el.find(ADD_EXCEPTIONS_BTN); + }) + .should('be.visible'); }; export const removeException = () => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_flyout/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_flyout/index.tsx index 6d01908732ec1..0ed810b4ad263 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_flyout/index.tsx @@ -410,27 +410,35 @@ export const EditExceptionFlyout = memo(function EditExceptionFlyout({ )} - {updateError != null && ( - - - - )} - {hasVersionConflict && ( - - -

{i18n.VERSION_CONFLICT_ERROR_DESCRIPTION}

-
-
- )} - {updateError == null && ( - + + + {hasVersionConflict && ( + <> + +

{i18n.VERSION_CONFLICT_ERROR_DESCRIPTION}

+
+ + + )} + {updateError != null && ( + <> + + + + )} + {updateError === null && ( {i18n.CANCEL} @@ -446,8 +454,8 @@ export const EditExceptionFlyout = memo(function EditExceptionFlyout({ {i18n.EDIT_EXCEPTION_SAVE_BUTTON} -
- )} + )} +
); });