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 new file mode 100644 index 0000000000000..db486a6478e1a --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/isolate.cy.ts @@ -0,0 +1,286 @@ +/* + * 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 { APP_CASES_PATH, APP_ENDPOINTS_PATH } from '../../../../../common/constants'; +import { closeAllToasts } from '../../tasks/close_all_toasts'; +import { + checkEndpointListForIsolatedHosts, + checkFlyoutEndpointIsolation, + createAgentPolicyTask, + filterOutEndpoints, + filterOutIsolatedHosts, + isolateHostWithComment, + openAlertDetails, + openCaseAlertDetails, + releaseHostWithComment, + toggleRuleOffAndOn, + visitRuleAlerts, + waitForReleaseOption, +} from '../../tasks/isolate'; +import { cleanupCase, cleanupRule, loadCase, loadRule } from '../../tasks/api_fixtures'; +import { ENDPOINT_VM_NAME } from '../../tasks/common'; +import { login } from '../../tasks/login'; +import type { IndexedFleetEndpointPolicyResponse } from '../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy'; +import { + getAgentByHostName, + getEndpointIntegrationVersion, + reassignAgentPolicy, +} from '../../tasks/fleet'; + +describe('Isolate command', () => { + const endpointHostname = Cypress.env(ENDPOINT_VM_NAME); + const isolateComment = `Isolating ${endpointHostname}`; + const releaseComment = `Releasing ${endpointHostname}`; + + beforeEach(() => { + login(); + }); + + describe('From manage', () => { + let response: IndexedFleetEndpointPolicyResponse; + let initialAgentData: Agent; + + before(() => { + getAgentByHostName(endpointHostname).then((agentData) => { + initialAgentData = agentData; + }); + + getEndpointIntegrationVersion().then((version) => { + createAgentPolicyTask(version, (data) => { + response = data; + }); + }); + }); + + after(() => { + if (initialAgentData?.policy_id) { + reassignAgentPolicy(initialAgentData.id, initialAgentData.policy_id); + } + if (response) { + cy.task('deleteIndexedFleetEndpointPolicies', response); + } + }); + + it('should allow filtering endpoint by Isolated status', () => { + cy.visit(APP_ENDPOINTS_PATH); + closeAllToasts(); + checkEndpointListForIsolatedHosts(false); + + filterOutIsolatedHosts(); + cy.contains('No items found'); + cy.getByTestSubj('adminSearchBar').click().type('{selectall}{backspace}'); + cy.getByTestSubj('querySubmitButton').click(); + cy.getByTestSubj('endpointTableRowActions').click(); + cy.getByTestSubj('isolateLink').click(); + + cy.contains(`Isolate host ${endpointHostname} from network.`); + cy.getByTestSubj('endpointHostIsolationForm'); + cy.getByTestSubj('host_isolation_comment').type(isolateComment); + cy.getByTestSubj('hostIsolateConfirmButton').click(); + cy.contains(`Isolation on host ${endpointHostname} successfully submitted`); + cy.getByTestSubj('euiFlyoutCloseButton').click(); + cy.getByTestSubj('rowHostStatus-actionStatuses').should('contain.text', 'Isolated'); + filterOutIsolatedHosts(); + + checkEndpointListForIsolatedHosts(); + + cy.getByTestSubj('endpointTableRowActions').click(); + cy.getByTestSubj('unIsolateLink').click(); + releaseHostWithComment(releaseComment, endpointHostname); + cy.contains('Confirm').click(); + cy.getByTestSubj('euiFlyoutCloseButton').click(); + cy.getByTestSubj('adminSearchBar').click().type('{selectall}{backspace}'); + cy.getByTestSubj('querySubmitButton').click(); + checkEndpointListForIsolatedHosts(false); + }); + }); + + describe('From alerts', () => { + let response: IndexedFleetEndpointPolicyResponse; + let initialAgentData: Agent; + let ruleId: string; + let ruleName: string; + + before(() => { + getAgentByHostName(endpointHostname).then((agentData) => { + initialAgentData = agentData; + }); + + getEndpointIntegrationVersion().then((version) => { + createAgentPolicyTask(version, (data) => { + response = data; + }); + }); + loadRule(false).then((data) => { + ruleId = data.id; + ruleName = data.name; + }); + }); + + after(() => { + if (initialAgentData?.policy_id) { + reassignAgentPolicy(initialAgentData.id, initialAgentData.policy_id); + } + if (response) { + cy.task('deleteIndexedFleetEndpointPolicies', response); + } + if (ruleId) { + cleanupRule(ruleId); + } + }); + + it('should have generated endpoint and rule', () => { + cy.visit(APP_ENDPOINTS_PATH); + cy.contains(endpointHostname).should('exist'); + + toggleRuleOffAndOn(ruleName); + }); + + it('should isolate and release host', () => { + visitRuleAlerts(ruleName); + + filterOutEndpoints(endpointHostname); + + closeAllToasts(); + openAlertDetails(); + + isolateHostWithComment(isolateComment, endpointHostname); + + cy.getByTestSubj('hostIsolateConfirmButton').click(); + cy.contains(`Isolation on host ${endpointHostname} successfully submitted`); + + cy.getByTestSubj('euiFlyoutCloseButton').click(); + openAlertDetails(); + + checkFlyoutEndpointIsolation(); + + releaseHostWithComment(releaseComment, endpointHostname); + cy.contains('Confirm').click(); + + cy.contains(`Release on host ${endpointHostname} successfully submitted`); + cy.getByTestSubj('euiFlyoutCloseButton').click(); + openAlertDetails(); + cy.getByTestSubj('event-field-agent.status').within(() => { + cy.get('[title="Isolated"]').should('not.exist'); + }); + }); + }); + + describe('From cases', () => { + let response: IndexedFleetEndpointPolicyResponse; + let initialAgentData: Agent; + let ruleId: string; + let ruleName: string; + let caseId: string; + + const caseOwner = 'securitySolution'; + + before(() => { + getAgentByHostName(endpointHostname).then((agentData) => { + initialAgentData = agentData; + }); + getEndpointIntegrationVersion().then((version) => { + createAgentPolicyTask(version, (data) => { + response = data; + }); + }); + + loadRule(false).then((data) => { + ruleId = data.id; + ruleName = data.name; + }); + loadCase(caseOwner).then((data) => { + caseId = data.id; + }); + }); + + beforeEach(() => { + login(); + }); + + after(() => { + if (initialAgentData?.policy_id) { + reassignAgentPolicy(initialAgentData.id, initialAgentData.policy_id); + } + if (response) { + cy.task('deleteIndexedFleetEndpointPolicies', response); + } + if (ruleId) { + cleanupRule(ruleId); + } + if (caseId) { + cleanupCase(caseId); + } + }); + + it('should have generated endpoint and rule', () => { + cy.visit(APP_ENDPOINTS_PATH); + cy.contains(endpointHostname).should('exist'); + + toggleRuleOffAndOn(ruleName); + }); + + it('should isolate and release host', () => { + visitRuleAlerts(ruleName); + filterOutEndpoints(endpointHostname); + closeAllToasts(); + + openAlertDetails(); + + cy.getByTestSubj('add-to-existing-case-action').click(); + cy.getByTestSubj(`cases-table-row-select-${caseId}`).click(); + cy.contains(`An alert was added to \"Test ${caseOwner} case`); + + cy.intercept('GET', `/api/cases/${caseId}/user_actions/_find*`).as('case'); + cy.visit(`${APP_CASES_PATH}/${caseId}`); + cy.wait('@case', { timeout: 30000 }).then(({ response: res }) => { + const caseAlertId = res?.body.userActions[1].id; + + closeAllToasts(); + openCaseAlertDetails(caseAlertId); + isolateHostWithComment(isolateComment, endpointHostname); + cy.getByTestSubj('hostIsolateConfirmButton').click(); + + cy.getByTestSubj('euiFlyoutCloseButton').click(); + + cy.getByTestSubj('user-actions-list').within(() => { + cy.contains(isolateComment); + cy.get('[aria-label="lock"]').should('exist'); + cy.get('[aria-label="lockOpen"]').should('not.exist'); + }); + + waitForReleaseOption(caseAlertId); + + releaseHostWithComment(releaseComment, endpointHostname); + + cy.contains('Confirm').click(); + + cy.contains(`Release on host ${endpointHostname} successfully submitted`); + cy.getByTestSubj('euiFlyoutCloseButton').click(); + + cy.getByTestSubj('user-actions-list').within(() => { + cy.contains(releaseComment); + cy.contains(isolateComment); + cy.get('[aria-label="lock"]').should('exist'); + cy.get('[aria-label="lockOpen"]').should('exist'); + }); + + openCaseAlertDetails(caseAlertId); + + cy.getByTestSubj('event-field-agent.status').then(($status) => { + if ($status.find('[title="Isolated"]').length > 0) { + cy.getByTestSubj('euiFlyoutCloseButton').click(); + cy.getByTestSubj(`comment-action-show-alert-${caseAlertId}`).click(); + cy.getByTestSubj('take-action-dropdown-btn').click(); + } + cy.get('[title="Isolated"]').should('not.exist'); + }); + }); + }); + }); +}); 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 579c0cab8c540..416987432fce3 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,6 +7,9 @@ import { getEndpointListPath } from '../../../common/routing'; import { + checkEndpointListForIsolatedHosts, + checkFlyoutEndpointIsolation, + filterOutIsolatedHosts, interceptActionRequests, isolateHostWithComment, openAlertDetails, @@ -67,18 +70,9 @@ describe('Isolate command', () => { it('should allow filtering endpoint by Isolated status', () => { cy.visit(APP_PATH + getEndpointListPath({ name: 'endpointList' })); closeAllToasts(); - cy.getByTestSubj('adminSearchBar') - .click() - .type('united.endpoint.Endpoint.state.isolation: true'); - cy.getByTestSubj('querySubmitButton').click(); + filterOutIsolatedHosts(); cy.contains('Showing 2 endpoints'); - cy.getByTestSubj('endpointListTable').within(() => { - cy.get('tbody tr').each(($tr) => { - cy.wrap($tr).within(() => { - cy.get('td').eq(1).should('contain.text', 'Isolated'); - }); - }); - }); + checkEndpointListForIsolatedHosts(); }); }); @@ -161,18 +155,8 @@ describe('Isolate command', () => { cy.getByTestSubj('euiFlyoutCloseButton').click(); cy.wait(1000); openAlertDetails(); - cy.getByTestSubj('event-field-agent.status').then(($status) => { - if ($status.find('[title="Isolated"]').length > 0) { - cy.contains('Release host').click(); - } else { - cy.getByTestSubj('euiFlyoutCloseButton').click(); - openAlertDetails(); - cy.getByTestSubj('event-field-agent.status').within(() => { - cy.contains('Isolated'); - }); - cy.contains('Release host').click(); - } - }); + + checkFlyoutEndpointIsolation(); releaseHostWithComment(releaseComment, hostname); diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/api_fixtures.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/api_fixtures.ts index 8d8df6318d215..3b8b7cfae6340 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/api_fixtures.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/api_fixtures.ts @@ -5,10 +5,8 @@ * 2.0. */ -import type { - RuleCreateProps, - RuleResponse, -} from '../../../../common/detection_engine/rule_schema'; +import type { CaseResponse } from '@kbn/cases-plugin/common'; +import type { RuleResponse } from '../../../../common/detection_engine/rule_schema'; import { request } from './common'; export const generateRandomStringName = (length: number) => @@ -18,9 +16,10 @@ export const cleanupRule = (id: string) => { request({ method: 'DELETE', url: `/api/detection_engine/rules?id=${id}` }); }; -export const loadRule = () => +export const loadRule = (includeResponseActions = true) => request({ method: 'POST', + url: `/api/detection_engine/rules`, body: { type: 'query', index: [ @@ -56,9 +55,35 @@ export const loadRule = () => actions: [], enabled: true, throttle: 'no_actions', - response_actions: [ - { params: { command: 'isolate', comment: 'Isolate host' }, action_type_id: '.endpoint' }, - ], - } as RuleCreateProps, - url: `/api/detection_engine/rules`, + ...(includeResponseActions + ? { + response_actions: [ + { + params: { command: 'isolate', comment: 'Isolate host' }, + action_type_id: '.endpoint', + }, + ], + } + : {}), + }, }).then((response) => response.body); + +export const loadCase = (owner: string) => + request({ + method: 'POST', + url: '/api/cases', + body: { + title: `Test ${owner} case ${generateRandomStringName(1)[0]}`, + tags: [], + severity: 'low', + description: 'Test security case', + assignees: [], + connector: { id: 'none', name: 'none', type: '.none', fields: null }, + settings: { syncAlerts: true }, + owner, + }, + }).then((response) => response.body); + +export const cleanupCase = (id: string) => { + request({ method: 'DELETE', url: '/api/cases', qs: { ids: JSON.stringify([id]) } }); +}; 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 b00b567852026..4644faaca2abf 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 @@ -5,6 +5,7 @@ * 2.0. */ +import type { IndexedFleetEndpointPolicyResponse } from '../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy'; import type { ActionDetails } from '../../../../common/endpoint/types'; const API_ENDPOINT_ACTION_PATH = '/api/endpoint/action/*'; @@ -52,6 +53,7 @@ export const openCaseAlertDetails = (alertId: string): void => { cy.getByTestSubj(`comment-action-show-alert-${alertId}`).click(); cy.getByTestSubj('take-action-dropdown-btn').click(); }; + export const waitForReleaseOption = (alertId: string): void => { openCaseAlertDetails(alertId); cy.getByTestSubj('event-field-agent.status').then(($status) => { @@ -67,3 +69,73 @@ export const waitForReleaseOption = (alertId: string): void => { } }); }; + +export const visitRuleAlerts = (ruleName: string) => { + cy.visit('/app/security/rules'); + cy.contains(ruleName).click(); +}; +export const checkFlyoutEndpointIsolation = (): void => { + cy.getByTestSubj('event-field-agent.status').then(($status) => { + if ($status.find('[title="Isolated"]').length > 0) { + cy.contains('Release host').click(); + } else { + cy.getByTestSubj('euiFlyoutCloseButton').click(); + cy.wait(5000); + openAlertDetails(); + cy.getByTestSubj('event-field-agent.status').within(() => { + cy.contains('Isolated'); + }); + cy.contains('Release host').click(); + } + }); +}; + +export const toggleRuleOffAndOn = (ruleName: string): void => { + cy.visit('/app/security/rules'); + cy.wait(2000); + cy.contains(ruleName) + .parents('tr') + .within(() => { + cy.getByTestSubj('ruleSwitch').should('have.attr', 'aria-checked', 'true'); + cy.getByTestSubj('ruleSwitch').click(); + cy.getByTestSubj('ruleSwitch').should('have.attr', 'aria-checked', 'false'); + cy.getByTestSubj('ruleSwitch').click(); + cy.getByTestSubj('ruleSwitch').should('have.attr', 'aria-checked', 'true'); + }); +}; + +export const filterOutEndpoints = (endpointHostname: string): void => { + cy.getByTestSubj('filters-global-container').within(() => { + cy.getByTestSubj('queryInput').click().type(`host.hostname : "${endpointHostname}"`); + cy.getByTestSubj('querySubmitButton').click(); + }); +}; + +export const createAgentPolicyTask = ( + version: string, + cb: (response: IndexedFleetEndpointPolicyResponse) => void +) => { + const policyName = `Reassign ${Math.random().toString(36).substring(2, 7)}`; + + cy.task('indexFleetEndpointPolicy', { + policyName, + endpointPackageVersion: version, + agentPolicyName: policyName, + }).then(cb); +}; + +export const filterOutIsolatedHosts = (): void => { + cy.getByTestSubj('adminSearchBar').click().type('united.endpoint.Endpoint.state.isolation: true'); + cy.getByTestSubj('querySubmitButton').click(); +}; + +export const checkEndpointListForIsolatedHosts = (expectIsolated = true): void => { + const chainer = expectIsolated ? 'contain.text' : 'not.contain.text'; + cy.getByTestSubj('endpointListTable').within(() => { + cy.get('tbody tr').each(($tr) => { + cy.wrap($tr).within(() => { + cy.get('td').eq(1).should(chainer, 'Isolated'); + }); + }); + }); +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/elastic_endpoint.ts b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/elastic_endpoint.ts index e6de63c7ba510..68ff4951f77d4 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/elastic_endpoint.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/elastic_endpoint.ts @@ -57,7 +57,7 @@ export const enrollEndpointHost = async (): Promise => { try { const uniqueId = Math.random().toString().substring(2, 6); - const username = userInfo().username.toLowerCase(); + const username = userInfo().username.toLowerCase().replace('.', '-'); // Multipass doesn't like periods in username const policyId: string = policy || (await getOrCreateAgentPolicyId()); if (!policyId) {