From eccff358de975b7be31ba680d21b77aa03e760aa Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Thu, 27 Apr 2023 10:27:21 +0200 Subject: [PATCH] [Defend Workflows][E2E]Endpoint e2e response console multipass (#155519) This PR adds e2e test run on real endpoint for coverage of isolate, processes, kill-process and suspend-process commands from respond console. Depends on https://github.com/elastic/kibana/pull/155360 (cherry picked from commit d80fdd6bceec438cae572ba13eae3ee3a9d3c5c3) --- .../cypress/e2e/endpoint/isolate.cy.ts | 36 ++-- .../e2e/endpoint/response_console.cy.ts | 170 ++++++++++++++++++ .../cypress/e2e/mocked_data/isolate.cy.ts | 4 +- .../management/cypress/tasks/isolate.ts | 16 +- .../cypress/tasks/response_console.ts | 58 ++++++ 5 files changed, 258 insertions(+), 26 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/response_console.cy.ts create mode 100644 x-pack/plugins/security_solution/public/management/cypress/tasks/response_console.ts diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/isolate.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/isolate.cy.ts index d42ca10964e84..9d77b1e40a800 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/isolate.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/isolate.cy.ts @@ -9,7 +9,8 @@ import type { Agent } from '@kbn/fleet-plugin/common'; import { APP_CASES_PATH, APP_ENDPOINTS_PATH } from '../../../../../common/constants'; import { closeAllToasts } from '../../tasks/close_all_toasts'; import { - checkEndpointListForIsolatedHosts, + checkEndpointListForOnlyIsolatedHosts, + checkEndpointListForOnlyUnIsolatedHosts, checkFlyoutEndpointIsolation, createAgentPolicyTask, filterOutEndpoints, @@ -50,11 +51,11 @@ describe('Isolate command', () => { initialAgentData = agentData; }); - getEndpointIntegrationVersion().then((version) => { - createAgentPolicyTask(version, (data) => { + getEndpointIntegrationVersion().then((version) => + createAgentPolicyTask(version).then((data) => { response = data; - }); - }); + }) + ); }); after(() => { @@ -66,11 +67,10 @@ describe('Isolate command', () => { } }); - // flaky - it.skip('should allow filtering endpoint by Isolated status', () => { + it('should allow filtering endpoint by Isolated status', () => { cy.visit(APP_ENDPOINTS_PATH); closeAllToasts(); - checkEndpointListForIsolatedHosts(false); + checkEndpointListForOnlyUnIsolatedHosts(); filterOutIsolatedHosts(); cy.contains('No items found'); @@ -88,7 +88,7 @@ describe('Isolate command', () => { cy.getByTestSubj('rowHostStatus-actionStatuses').should('contain.text', 'Isolated'); filterOutIsolatedHosts(); - checkEndpointListForIsolatedHosts(); + checkEndpointListForOnlyIsolatedHosts(); cy.getByTestSubj('endpointTableRowActions').click(); cy.getByTestSubj('unIsolateLink').click(); @@ -97,7 +97,7 @@ describe('Isolate command', () => { cy.getByTestSubj('euiFlyoutCloseButton').click(); cy.getByTestSubj('adminSearchBar').click().type('{selectall}{backspace}'); cy.getByTestSubj('querySubmitButton').click(); - checkEndpointListForIsolatedHosts(false); + checkEndpointListForOnlyUnIsolatedHosts(); }); }); @@ -112,11 +112,11 @@ describe('Isolate command', () => { initialAgentData = agentData; }); - getEndpointIntegrationVersion().then((version) => { - createAgentPolicyTask(version, (data) => { + getEndpointIntegrationVersion().then((version) => + createAgentPolicyTask(version).then((data) => { response = data; - }); - }); + }) + ); loadRule(false).then((data) => { ruleId = data.id; ruleName = data.name; @@ -185,11 +185,11 @@ describe('Isolate command', () => { getAgentByHostName(endpointHostname).then((agentData) => { initialAgentData = agentData; }); - getEndpointIntegrationVersion().then((version) => { - createAgentPolicyTask(version, (data) => { + getEndpointIntegrationVersion().then((version) => + createAgentPolicyTask(version).then((data) => { response = data; - }); - }); + }) + ); loadRule(false).then((data) => { ruleId = data.id; diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/response_console.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/response_console.cy.ts new file mode 100644 index 0000000000000..882cf45465111 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/response_console.cy.ts @@ -0,0 +1,170 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Agent } from '@kbn/fleet-plugin/common'; +import { + inputConsoleCommand, + openResponseConsoleFromEndpointList, + performCommandInputChecks, + submitCommand, + waitForCommandToBeExecuted, + waitForEndpointListPageToBeLoaded, +} from '../../tasks/response_console'; +import type { IndexedFleetEndpointPolicyResponse } from '../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy'; +import { + getAgentByHostName, + getEndpointIntegrationVersion, + reassignAgentPolicy, +} from '../../tasks/fleet'; +import { + checkEndpointListForOnlyIsolatedHosts, + checkEndpointListForOnlyUnIsolatedHosts, + createAgentPolicyTask, +} from '../../tasks/isolate'; +import { login } from '../../tasks/login'; +import { ENDPOINT_VM_NAME } from '../../tasks/common'; + +describe('Response console', () => { + const endpointHostname = Cypress.env(ENDPOINT_VM_NAME); + + beforeEach(() => { + login(); + }); + + describe('User journey for Isolate command: isolate and release an endpoint', () => { + let response: IndexedFleetEndpointPolicyResponse; + let initialAgentData: Agent; + + before(() => { + getAgentByHostName(endpointHostname).then((agentData) => { + initialAgentData = agentData; + }); + + getEndpointIntegrationVersion().then((version) => + createAgentPolicyTask(version).then((data) => { + response = data; + }) + ); + }); + + after(() => { + if (initialAgentData?.policy_id) { + reassignAgentPolicy(initialAgentData.id, initialAgentData.policy_id); + } + if (response) { + cy.task('deleteIndexedFleetEndpointPolicies', response); + } + }); + + it('should isolate host from response console', () => { + waitForEndpointListPageToBeLoaded(endpointHostname); + checkEndpointListForOnlyUnIsolatedHosts(); + openResponseConsoleFromEndpointList(); + performCommandInputChecks('isolate'); + submitCommand(); + waitForCommandToBeExecuted(); + waitForEndpointListPageToBeLoaded(endpointHostname); + checkEndpointListForOnlyIsolatedHosts(); + }); + + it('should release host from response console', () => { + waitForEndpointListPageToBeLoaded(endpointHostname); + checkEndpointListForOnlyIsolatedHosts(); + openResponseConsoleFromEndpointList(); + performCommandInputChecks('release'); + submitCommand(); + waitForCommandToBeExecuted(); + waitForEndpointListPageToBeLoaded(endpointHostname); + checkEndpointListForOnlyUnIsolatedHosts(); + }); + }); + + describe('User journey for Processes commands: list, kill and suspend process.', () => { + let response: IndexedFleetEndpointPolicyResponse; + let initialAgentData: Agent; + let cronPID: string; + let newCronPID: string; + + before(() => { + getAgentByHostName(endpointHostname).then((agentData) => { + initialAgentData = agentData; + }); + + getEndpointIntegrationVersion().then((version) => + createAgentPolicyTask(version).then((data) => { + response = data; + }) + ); + }); + + after(() => { + if (initialAgentData?.policy_id) { + reassignAgentPolicy(initialAgentData.id, initialAgentData.policy_id); + } + if (response) { + cy.task('deleteIndexedFleetEndpointPolicies', response); + } + }); + + it('"processes" - should obtain a list of processes', () => { + waitForEndpointListPageToBeLoaded(endpointHostname); + openResponseConsoleFromEndpointList(); + performCommandInputChecks('processes'); + submitCommand(); + cy.contains('Action pending.').should('exist'); + cy.getByTestSubj('getProcessesSuccessCallout', { timeout: 120000 }).within(() => { + ['USER', 'PID', 'ENTITY ID', 'COMMAND'].forEach((header) => { + cy.contains(header); + }); + + cy.get('tbody > tr').should('have.length.greaterThan', 0); + cy.get('tbody > tr > td').should('contain', '/usr/sbin/cron'); + cy.get('tbody > tr > td') + .contains('/usr/sbin/cron') + .parents('td') + .siblings('td') + .eq(1) + .find('span') + .then((span) => { + cronPID = span.text(); + }); + }); + }); + + it('"kill-process --pid" - should kill a process', () => { + waitForEndpointListPageToBeLoaded(endpointHostname); + openResponseConsoleFromEndpointList(); + inputConsoleCommand(`kill-process --pid ${cronPID}`); + submitCommand(); + waitForCommandToBeExecuted(); + + performCommandInputChecks('processes'); + submitCommand(); + + cy.getByTestSubj('getProcessesSuccessCallout', { timeout: 120000 }).within(() => { + cy.get('tbody > tr > td') + .contains('/usr/sbin/cron') + .parents('td') + .siblings('td') + .eq(1) + .find('span') + .then((span) => { + newCronPID = span.text(); + }); + }); + expect(newCronPID).to.not.equal(cronPID); + }); + + it('"suspend-process --pid" - should suspend a process', () => { + waitForEndpointListPageToBeLoaded(endpointHostname); + openResponseConsoleFromEndpointList(); + inputConsoleCommand(`suspend-process --pid ${newCronPID}`); + submitCommand(); + waitForCommandToBeExecuted(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/isolate.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/isolate.cy.ts index 416987432fce3..392e2ed310ccd 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/isolate.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/isolate.cy.ts @@ -7,7 +7,7 @@ import { getEndpointListPath } from '../../../common/routing'; import { - checkEndpointListForIsolatedHosts, + checkEndpointListForOnlyIsolatedHosts, checkFlyoutEndpointIsolation, filterOutIsolatedHosts, interceptActionRequests, @@ -72,7 +72,7 @@ describe('Isolate command', () => { closeAllToasts(); filterOutIsolatedHosts(); cy.contains('Showing 2 endpoints'); - checkEndpointListForIsolatedHosts(); + checkEndpointListForOnlyIsolatedHosts(); }); }); diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/isolate.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/isolate.ts index 4644faaca2abf..8a05d790e19bd 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/isolate.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/isolate.ts @@ -112,16 +112,15 @@ export const filterOutEndpoints = (endpointHostname: string): void => { }; export const createAgentPolicyTask = ( - version: string, - cb: (response: IndexedFleetEndpointPolicyResponse) => void -) => { + version: string +): Cypress.Chainable => { const policyName = `Reassign ${Math.random().toString(36).substring(2, 7)}`; - cy.task('indexFleetEndpointPolicy', { + return cy.task('indexFleetEndpointPolicy', { policyName, endpointPackageVersion: version, agentPolicyName: policyName, - }).then(cb); + }); }; export const filterOutIsolatedHosts = (): void => { @@ -129,7 +128,7 @@ export const filterOutIsolatedHosts = (): void => { cy.getByTestSubj('querySubmitButton').click(); }; -export const checkEndpointListForIsolatedHosts = (expectIsolated = true): void => { +const checkEndpointListForIsolatedHosts = (expectIsolated: boolean): void => { const chainer = expectIsolated ? 'contain.text' : 'not.contain.text'; cy.getByTestSubj('endpointListTable').within(() => { cy.get('tbody tr').each(($tr) => { @@ -139,3 +138,8 @@ export const checkEndpointListForIsolatedHosts = (expectIsolated = true): void = }); }); }; + +export const checkEndpointListForOnlyUnIsolatedHosts = (): void => + checkEndpointListForIsolatedHosts(false); +export const checkEndpointListForOnlyIsolatedHosts = (): void => + checkEndpointListForIsolatedHosts(true); diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/response_console.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/response_console.ts new file mode 100644 index 0000000000000..5932bc6abf3ec --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/response_console.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { closeAllToasts } from './close_all_toasts'; +import { APP_ENDPOINTS_PATH } from '../../../../common/constants'; + +export const waitForEndpointListPageToBeLoaded = (endpointHostname: string): void => { + cy.visit(APP_ENDPOINTS_PATH); + closeAllToasts(); + cy.contains(endpointHostname).should('exist'); +}; +export const openResponseConsoleFromEndpointList = (): void => { + cy.getByTestSubj('endpointTableRowActions').first().click(); + cy.contains('Respond').click(); +}; + +export const inputConsoleCommand = (command: string): void => { + cy.getByTestSubj('endpointResponseActionsConsole-inputCapture').click().type(command); +}; + +export const clearConsoleCommandInput = (): void => { + cy.getByTestSubj('endpointResponseActionsConsole-inputCapture') + .click() + .type(`{selectall}{backspace}`); +}; + +export const selectCommandFromHelpMenu = (command: string): void => { + cy.getByTestSubj('endpointResponseActionsConsole-header-helpButton').click(); + cy.getByTestSubj( + `endpointResponseActionsConsole-commandList-Responseactions-${command}-addToInput` + ).click(); +}; + +export const checkInputForCommandPresence = (command: string): void => { + cy.getByTestSubj('endpointResponseActionsConsole-cmdInput-leftOfCursor') + .invoke('text') + .should('eq', `${command} `); // command in the cli input is followed by a space +}; + +export const submitCommand = (): void => { + cy.getByTestSubj('endpointResponseActionsConsole-inputTextSubmitButton').click(); +}; + +export const waitForCommandToBeExecuted = (): void => { + cy.contains('Action pending.').should('exist'); + cy.contains('Action completed.', { timeout: 120000 }).should('exist'); +}; + +export const performCommandInputChecks = (command: string) => { + inputConsoleCommand(command); + clearConsoleCommandInput(); + selectCommandFromHelpMenu(command); + checkInputForCommandPresence(command); +};