diff --git a/x-pack/plugins/security_solution/cypress/fixtures/cidr_list.txt b/x-pack/plugins/security_solution/cypress/fixtures/cidr_list.txt new file mode 100644 index 0000000000000..2a147ab8b6c46 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/fixtures/cidr_list.txt @@ -0,0 +1 @@ +192.168.100.14/24 diff --git a/x-pack/plugins/security_solution/cypress/fixtures/ip_list.txt b/x-pack/plugins/security_solution/cypress/fixtures/ip_list.txt new file mode 100644 index 0000000000000..b910649a18c3f --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/fixtures/ip_list.txt @@ -0,0 +1,3 @@ +127.0.0.1 +127.0.0.2 +127.0.0.3 diff --git a/x-pack/plugins/security_solution/cypress/integration/value_lists.spec.ts b/x-pack/plugins/security_solution/cypress/integration/value_lists.spec.ts index 2804a8ac2ea8c..f4de6d978a70d 100644 --- a/x-pack/plugins/security_solution/cypress/integration/value_lists.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/value_lists.spec.ts @@ -17,27 +17,207 @@ import { openValueListsModal, selectValueListsFile, uploadValueList, + selectValueListType, + deleteAllValueListsFromUI, + closeValueListsModal, + importValueList, + deleteValueListsFile, + exportValueList, } from '../tasks/lists'; import { VALUE_LISTS_TABLE, VALUE_LISTS_ROW } from '../screens/lists'; describe('value lists', () => { describe('management modal', () => { - it('creates a keyword list from an uploaded file', () => { + beforeEach(() => { loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); waitForListsIndexToBeCreated(); goToManageAlertsDetectionRules(); waitForValueListsModalToBeLoaded(); + }); + + afterEach(() => { + deleteAllValueListsFromUI(); + }); + + it('can open and close the modal', () => { openValueListsModal(); - selectValueListsFile(); - uploadValueList(); + closeValueListsModal(); + }); + + describe('create list types', () => { + beforeEach(() => { + openValueListsModal(); + }); + + it('creates a "keyword" list from an uploaded file', () => { + const listName = 'value_list.txt'; + selectValueListType('keyword'); + selectValueListsFile(listName); + uploadValueList(); + + cy.get(VALUE_LISTS_TABLE) + .find(VALUE_LISTS_ROW) + .should(($row) => { + expect($row.text()).to.contain(listName); + expect($row.text()).to.contain('Keywords'); + }); + }); + + it('creates a "text" list from an uploaded file', () => { + const listName = 'value_list.txt'; + selectValueListType('text'); + selectValueListsFile(listName); + uploadValueList(); + + cy.get(VALUE_LISTS_TABLE) + .find(VALUE_LISTS_ROW) + .should(($row) => { + expect($row.text()).to.contain(listName); + expect($row.text()).to.contain('Text'); + }); + }); + + it('creates a "ip" list from an uploaded file', () => { + const listName = 'ip_list.txt'; + selectValueListType('ip'); + selectValueListsFile(listName); + uploadValueList(); + + cy.get(VALUE_LISTS_TABLE) + .find(VALUE_LISTS_ROW) + .should(($row) => { + expect($row.text()).to.contain(listName); + expect($row.text()).to.contain('IP addresses'); + }); + }); + + it('creates a "ip_range" list from an uploaded file', () => { + const listName = 'cidr_list.txt'; + selectValueListType('ip_range'); + selectValueListsFile(listName); + uploadValueList(); + + cy.get(VALUE_LISTS_TABLE) + .find(VALUE_LISTS_ROW) + .should(($row) => { + expect($row.text()).to.contain(listName); + expect($row.text()).to.contain('IP ranges'); + }); + }); + }); + + describe('delete list types', () => { + it('deletes a "keyword" list from an uploaded file', () => { + const listName = 'value_list.txt'; + importValueList(listName, 'keyword'); + openValueListsModal(); + deleteValueListsFile(listName); + cy.get(VALUE_LISTS_TABLE) + .find(VALUE_LISTS_ROW) + .should(($row) => { + expect($row.text()).not.to.contain(listName); + }); + }); + + it('deletes a "text" list from an uploaded file', () => { + const listName = 'value_list.txt'; + importValueList(listName, 'text'); + openValueListsModal(); + deleteValueListsFile(listName); + cy.get(VALUE_LISTS_TABLE) + .find(VALUE_LISTS_ROW) + .should(($row) => { + expect($row.text()).not.to.contain(listName); + }); + }); + + it('deletes a "ip" from an uploaded file', () => { + const listName = 'ip_list.txt'; + importValueList(listName, 'ip'); + openValueListsModal(); + deleteValueListsFile(listName); + cy.get(VALUE_LISTS_TABLE) + .find(VALUE_LISTS_ROW) + .should(($row) => { + expect($row.text()).not.to.contain(listName); + }); + }); + + it('deletes a "ip_range" from an uploaded file', () => { + const listName = 'cidr_list.txt'; + importValueList(listName, 'ip_range'); + openValueListsModal(); + deleteValueListsFile(listName); + cy.get(VALUE_LISTS_TABLE) + .find(VALUE_LISTS_ROW) + .should(($row) => { + expect($row.text()).not.to.contain(listName); + }); + }); + }); + + describe('export list types', () => { + beforeEach(() => { + cy.server(); + cy.route('POST', '**/api/lists/items/_export?list_id=*').as('exportList'); + }); + + it('exports a "keyword" list from an uploaded file', () => { + const listName = 'value_list.txt'; + importValueList('value_list.txt', 'keyword'); + openValueListsModal(); + exportValueList(); + cy.wait('@exportList').then((xhr) => { + cy.fixture(listName).then((list: string) => { + const [lineOne, lineTwo] = list.split('\n'); + expect(xhr.responseBody).to.contain(lineOne); + expect(xhr.responseBody).to.contain(lineTwo); + }); + }); + }); + + it('exports a "text" list from an uploaded file', () => { + const listName = 'value_list.txt'; + importValueList(listName, 'text'); + openValueListsModal(); + exportValueList(); + cy.wait('@exportList').then((xhr) => { + cy.fixture(listName).then((list: string) => { + const [lineOne, lineTwo] = list.split('\n'); + expect(xhr.responseBody).to.contain(lineOne); + expect(xhr.responseBody).to.contain(lineTwo); + }); + }); + }); + + it('exports a "ip" list from an uploaded file', () => { + const listName = 'ip_list.txt'; + importValueList(listName, 'ip'); + openValueListsModal(); + exportValueList(); + cy.wait('@exportList').then((xhr) => { + cy.fixture(listName).then((list: string) => { + const [lineOne, lineTwo] = list.split('\n'); + expect(xhr.responseBody).to.contain(lineOne); + expect(xhr.responseBody).to.contain(lineTwo); + }); + }); + }); - cy.get(VALUE_LISTS_TABLE) - .find(VALUE_LISTS_ROW) - .should(($row) => { - expect($row.text()).to.contain('value_list.txt'); + it('exports a "ip_range" list from an uploaded file', () => { + const listName = 'cidr_list.txt'; + importValueList(listName, 'ip_range'); + openValueListsModal(); + exportValueList(); + cy.wait('@exportList').then((xhr) => { + cy.fixture(listName).then((list: string) => { + const [lineOne] = list.split('\n'); + expect(xhr.responseBody).to.contain(lineOne); + }); }); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/lists.ts b/x-pack/plugins/security_solution/cypress/screens/lists.ts index 35205a27e5a3c..78c5104b17efe 100644 --- a/x-pack/plugins/security_solution/cypress/screens/lists.ts +++ b/x-pack/plugins/security_solution/cypress/screens/lists.ts @@ -9,3 +9,9 @@ export const VALUE_LISTS_TABLE = '[data-test-subj="value-lists-table"]'; export const VALUE_LISTS_ROW = '.euiTableRow'; export const VALUE_LIST_FILE_PICKER = '[data-test-subj="value-list-file-picker"]'; export const VALUE_LIST_FILE_UPLOAD_BUTTON = '[data-test-subj="value-lists-form-import-action"]'; +export const VALUE_LIST_TYPE_SELECTOR = '[data-test-subj="value-lists-form-select-type-action"]'; +export const VALUE_LIST_DELETE_BUTTON = (name: string) => + `[data-test-subj="action-delete-value-list-${name}"]`; +export const VALUE_LIST_FILES = '[data-test-subj*="action-delete-value-list-"]'; +export const VALUE_LIST_CLOSE_BUTTON = '[data-test-subj="value-lists-modal-close-action"]'; +export const VALUE_LIST_EXPORT_BUTTON = '[data-test-subj="action-export-value-list"]'; diff --git a/x-pack/plugins/security_solution/cypress/support/commands.js b/x-pack/plugins/security_solution/cypress/support/commands.js index e13a76736205c..bb32461a6bca2 100644 --- a/x-pack/plugins/security_solution/cypress/support/commands.js +++ b/x-pack/plugins/security_solution/cypress/support/commands.js @@ -58,7 +58,7 @@ Cypress.Commands.add( }, (input, fileName, fileType = 'text/plain') => { cy.fixture(fileName).then((content) => { - const blob = Cypress.Blob.base64StringToBlob(content, fileType); + const blob = Cypress.Blob.base64StringToBlob(btoa(content), fileType); const testFile = new File([blob], fileName, { type: fileType }); const dataTransfer = new DataTransfer(); diff --git a/x-pack/plugins/security_solution/cypress/tasks/lists.ts b/x-pack/plugins/security_solution/cypress/tasks/lists.ts index 638c69c087adf..1ecfeaad06d46 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/lists.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/lists.ts @@ -6,8 +6,13 @@ import { VALUE_LISTS_MODAL_ACTIVATOR, + VALUE_LIST_CLOSE_BUTTON, + VALUE_LIST_DELETE_BUTTON, + VALUE_LIST_EXPORT_BUTTON, + VALUE_LIST_FILES, VALUE_LIST_FILE_PICKER, VALUE_LIST_FILE_UPLOAD_BUTTON, + VALUE_LIST_TYPE_SELECTOR, } from '../screens/lists'; export const waitForListsIndexToBeCreated = () => { @@ -23,14 +28,96 @@ export const waitForValueListsModalToBeLoaded = () => { cy.get(VALUE_LISTS_MODAL_ACTIVATOR).should('not.be.disabled'); }; -export const openValueListsModal = () => { - cy.get(VALUE_LISTS_MODAL_ACTIVATOR).click(); +export const openValueListsModal = (): Cypress.Chainable> => { + return cy.get(VALUE_LISTS_MODAL_ACTIVATOR).click(); }; -export const selectValueListsFile = () => { - cy.get(VALUE_LIST_FILE_PICKER).attachFile('value_list.txt').trigger('change', { force: true }); +export const closeValueListsModal = (): Cypress.Chainable> => { + return cy.get(VALUE_LIST_CLOSE_BUTTON).click(); }; -export const uploadValueList = () => { - cy.get(VALUE_LIST_FILE_UPLOAD_BUTTON).click(); +export const selectValueListsFile = (file: string): Cypress.Chainable> => { + return cy.get(VALUE_LIST_FILE_PICKER).attachFile(file).trigger('change', { force: true }); +}; + +export const deleteValueListsFile = (file: string): Cypress.Chainable> => { + return cy.get(VALUE_LIST_DELETE_BUTTON(file)).click(); +}; + +export const selectValueListType = (type: string): Cypress.Chainable> => { + return cy.get(VALUE_LIST_TYPE_SELECTOR).select(type); +}; + +export const uploadValueList = (): Cypress.Chainable> => { + return cy.get(VALUE_LIST_FILE_UPLOAD_BUTTON).click(); +}; + +export const exportValueList = (): Cypress.Chainable> => { + return cy.get(VALUE_LIST_EXPORT_BUTTON).click(); +}; + +/** + * Given an array of value lists this will delete them all using Cypress Request and the lists REST API + * Ref: https://www.elastic.co/guide/en/security/current/lists-api-delete-container.html + */ +export const deleteValueLists = (lists: string[]): Array> => { + return lists.map((list) => deleteValueList(list)); +}; + +/** + * Given a single value list this will delete it using Cypress Request and lists REST API + * Ref: https://www.elastic.co/guide/en/security/current/lists-api-delete-container.html + */ +export const deleteValueList = (list: string): Cypress.Chainable => { + return cy.request({ + method: 'DELETE', + url: `api/lists?id=${list}`, + headers: { 'kbn-xsrf': 'delete-lists' }, + }); +}; + +/** + * Imports a single value list file this using Cypress Request and lists REST API + * Ref: https://www.elastic.co/guide/en/security/current/lists-api-import-list-items.html + */ +export const importValueList = ( + file: string, + type: string +): Cypress.Chainable => { + return cy.fixture(file).then((data) => { + return cy.request({ + method: 'POST', + url: `api/lists/items/_import?type=${type}`, + encoding: 'binary', + headers: { + 'kbn-xsrf': 'upload-value-lists', + 'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundaryJLrRH89J8QVArZyv', + }, + body: `------WebKitFormBoundaryJLrRH89J8QVArZyv\nContent-Disposition: form-data; name="file"; filename="${file}"\n\n${data}`, + }); + }); +}; + +/** + * If you are on the value lists from the UI, this will loop over all the HTML elements + * that have action-delete-value-list-${list_name} and delete all of those value lists + * using Cypress Request and the lists REST API. + * If the UI does not contain any value based lists this will not fail. If the UI does + * contain value based lists but the backend does not return a success on DELETE then this + * will cause errors. + * Ref: https://www.elastic.co/guide/en/security/current/lists-api-delete-container.html + */ +export const deleteAllValueListsFromUI = (): Array> => { + const lists = Cypress.$(VALUE_LIST_FILES) + .toArray() + .reduce((accum, $el) => { + const attribute = $el.getAttribute('data-test-subj'); + if (attribute != null) { + const list = attribute.substr('data-test-subj-value-list'.length); + return [...accum, list]; + } else { + return accum; + } + }, []); + return deleteValueLists(lists); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx index 5baa0ee5569d2..09f902b155b56 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx @@ -145,6 +145,7 @@ export const ValueListsFormComponent: React.FC = ({ onError { await waitFor(() => { container - .find('button[data-test-subj="action-delete-value-list"]') + .find('button[data-test-subj="action-delete-value-list-some name"]') .first() .simulate('click'); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table_helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table_helpers.tsx index 0b9b30aa48f81..4b65790c18f9e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table_helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table_helpers.tsx @@ -81,7 +81,7 @@ export const buildColumns = ( ) : ( onDelete(item)} />