diff --git a/x-pack/plugins/cases/public/components/user_actions/comment/actions.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/actions.tsx index 5b17b05a45f68..dccf5ae0b91a2 100644 --- a/x-pack/plugins/cases/public/components/user_actions/comment/actions.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/comment/actions.tsx @@ -34,6 +34,7 @@ export const createActionAttachmentUserActionBuilder = ({ // TODO: Fix this manually. Issue #123375 // eslint-disable-next-line react/display-name build: () => { + const actionIconName = comment.actions.type === 'isolate' ? 'lock' : 'lockOpen'; return [ { username: ( @@ -52,7 +53,8 @@ export const createActionAttachmentUserActionBuilder = ({ ), 'data-test-subj': 'endpoint-action', timestamp: , - timelineAvatar: comment.actions.type === 'isolate' ? 'lock' : 'lockOpen', + timelineAvatar: actionIconName, + timelineAvatarAriaLabel: actionIconName, actions: , children: comment.comment.trim().length > 0 && ( diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts index e5a44c46f5afc..bc429feb208d7 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts @@ -20,6 +20,7 @@ export interface GetCustomEndpointMetadataGeneratorOptions { version: string; /** OS type for the generated endpoint hosts */ os: 'macOS' | 'windows' | 'linux'; + isolation: boolean; } /** @@ -33,6 +34,7 @@ export class EndpointMetadataGenerator extends BaseDataGenerator { static custom({ version, os, + isolation, }: Partial = {}): typeof EndpointMetadataGenerator { return class extends EndpointMetadataGenerator { generate(overrides: DeepPartial = {}): HostMetadataInterface { @@ -54,6 +56,9 @@ export class EndpointMetadataGenerator extends BaseDataGenerator { set(overrides, 'host.os', EndpointMetadataGenerator.windowsOSFields); } } + if (isolation !== undefined) { + set(overrides, 'Endpoint.state.isolation', isolation); + } return super.generate(overrides); } @@ -104,10 +109,10 @@ export class EndpointMetadataGenerator extends BaseDataGenerator { /** Generate an Endpoint host metadata document */ generate(overrides: DeepPartial = {}): HostMetadataInterface { const ts = overrides['@timestamp'] ?? new Date().getTime(); - const hostName = this.randomHostname(); + const hostName = overrides?.host?.hostname ?? this.randomHostname(); const agentVersion = overrides?.agent?.version ?? this.randomVersion(); const agentId = this.seededUUIDv4(); - const isIsolated = this.randomBoolean(0.3); + const isIsolated = overrides?.Endpoint?.state?.isolation ?? this.randomBoolean(0.3); const capabilities: EndpointCapabilities[] = ['isolation']; // v8.4 introduced additional endpoint capabilities diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_rule_alert_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_rule_alert_generator.ts index 5578c179ba1f5..8396f86a45e97 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_rule_alert_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_rule_alert_generator.ts @@ -37,6 +37,8 @@ export class EndpointRuleAlertGenerator extends BaseDataGenerator { const endpointMetadataGenerator = new EndpointMetadataGenerator(); const endpointMetadata = endpointMetadataGenerator.generate({ agent: { version: kibanaPackageJson.version }, + host: { hostname: overrides?.host?.hostname }, + Endpoint: { state: { isolation: overrides?.Endpoint?.state?.isolation } }, }); const now = overrides['@timestamp'] ?? new Date().toISOString(); const endpointAgentId = overrides?.agent?.id ?? this.seededUUIDv4(); diff --git a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts index f9f96e650c056..684694bdb5c9a 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts @@ -75,6 +75,7 @@ export interface IndexedHostsResponse * @param policyResponseIndex * @param enrollFleet * @param generator + * @param disableEndpointActionsForHost */ export async function indexEndpointHostDocs({ numDocs, @@ -86,6 +87,7 @@ export async function indexEndpointHostDocs({ policyResponseIndex, enrollFleet, generator, + withResponseActions = true, }: { numDocs: number; client: Client; @@ -96,6 +98,7 @@ export async function indexEndpointHostDocs({ policyResponseIndex: string; enrollFleet: boolean; generator: EndpointDocGenerator; + withResponseActions?: boolean; }): Promise { const timeBetweenDocs = 6 * 3600 * 1000; // 6 hours between metadata documents const timestamp = new Date().getTime(); @@ -190,13 +193,15 @@ export async function indexEndpointHostDocs({ }, }; - // Create some fleet endpoint actions and .logs-endpoint actions for this Host - const actionsResponse = await indexEndpointAndFleetActionsForHost( - client, - hostMetadata, - undefined - ); - mergeAndAppendArrays(response, actionsResponse); + if (withResponseActions) { + // Create some fleet endpoint actions and .logs-endpoint actions for this Host + const actionsResponse = await indexEndpointAndFleetActionsForHost( + client, + hostMetadata, + undefined + ); + mergeAndAppendArrays(response, actionsResponse); + } } await client diff --git a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_rule_alerts.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_rule_alerts.ts index 74e9d82a714e9..1c5883c052135 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_rule_alerts.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_rule_alerts.ts @@ -22,6 +22,8 @@ import { EndpointRuleAlertGenerator } from '../data_generators/endpoint_rule_ale export interface IndexEndpointRuleAlertsOptions { esClient: Client; endpointAgentId: string; + endpointHostname?: string; + endpointIsolated?: boolean; count?: number; log?: ToolingLog; } @@ -40,12 +42,16 @@ export interface DeletedIndexedEndpointRuleAlerts { * written them to for a given endpoint * @param esClient * @param endpointAgentId + * @param endpointHostname + * @param endpointIsolated * @param count * @param log */ export const indexEndpointRuleAlerts = async ({ esClient, endpointAgentId, + endpointHostname, + endpointIsolated, count = 1, log = new ToolingLog(), }: IndexEndpointRuleAlertsOptions): Promise => { @@ -57,7 +63,11 @@ export const indexEndpointRuleAlerts = async ({ const indexedAlerts: estypes.IndexResponse[] = []; for (let n = 0; n < count; n++) { - const alert = alertsGenerator.generate({ agent: { id: endpointAgentId } }); + const alert = alertsGenerator.generate({ + agent: { id: endpointAgentId }, + host: { hostname: endpointHostname }, + ...(endpointIsolated ? { Endpoint: { state: { isolation: endpointIsolated } } } : {}), + }); const indexedAlert = await esClient.index({ index: `${DEFAULT_ALERTS_INDEX}-default`, refresh: 'wait_for', diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index d867c25ececf7..76ee903eb6889 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -410,7 +410,9 @@ export class EndpointDocGenerator extends BaseDataGenerator { private createHostData(): CommonHostInfo { const { agent, elastic, host, Endpoint } = this.metadataGenerator.generate({ - Endpoint: { policy: { applied: this.randomChoice(APPLIED_POLICIES) } }, + Endpoint: { + policy: { applied: this.randomChoice(APPLIED_POLICIES) }, + }, }); return { agent, elastic, host, Endpoint }; diff --git a/x-pack/plugins/security_solution/common/endpoint/index_data.ts b/x-pack/plugins/security_solution/common/endpoint/index_data.ts index 77b3135c12353..db5039e5a72f0 100644 --- a/x-pack/plugins/security_solution/common/endpoint/index_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/index_data.ts @@ -48,6 +48,7 @@ export type IndexedHostsAndAlertsResponse = IndexedHostsResponse; * @param fleet * @param options * @param DocGenerator + * @param withResponseActions */ export async function indexHostsAndAlerts( client: Client, @@ -62,7 +63,8 @@ export async function indexHostsAndAlerts( alertsPerHost: number, fleet: boolean, options: TreeOptions = {}, - DocGenerator: typeof EndpointDocGenerator = EndpointDocGenerator + DocGenerator: typeof EndpointDocGenerator = EndpointDocGenerator, + withResponseActions = true ): Promise { const random = seedrandom(seed); const epmEndpointPackage = await getEndpointPackageInfo(kbnClient); @@ -114,6 +116,7 @@ export async function indexHostsAndAlerts( policyResponseIndex, enrollFleet: fleet, generator, + withResponseActions, }); mergeAndAppendArrays(response, indexedHosts); diff --git a/x-pack/plugins/security_solution/public/management/cypress/cypress.d.ts b/x-pack/plugins/security_solution/public/management/cypress/cypress.d.ts index a48498a7ee43b..c461f712b75b5 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/cypress.d.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/cypress.d.ts @@ -11,8 +11,11 @@ import type { CasePostRequest } from '@kbn/cases-plugin/common/api'; import type { IndexedEndpointPolicyResponse } from '../../../common/endpoint/data_loaders/index_endpoint_policy_response'; -import type { HostPolicyResponse } from '../../../common/endpoint/types'; -import type { IndexEndpointHostsCyTaskOptions } from './types'; +import type { + HostPolicyResponse, + LogsEndpointActionResponse, +} from '../../../common/endpoint/types'; +import type { IndexEndpointHostsCyTaskOptions, HostActionResponse } from './types'; import type { DeleteIndexedFleetEndpointPoliciesResponse, IndexedFleetEndpointPolicyResponse, @@ -115,6 +118,12 @@ declare global { arg: IndexedEndpointPolicyResponse, options?: Partial ): Chainable; + + task( + name: 'sendHostActionResponse', + arg: HostActionResponse, + options?: Partial + ): Chainable; } } } diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/endpoints.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/endpoints.cy.ts index 35b931675a6c2..d5c946af96c14 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/endpoints.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/endpoints.cy.ts @@ -5,18 +5,31 @@ * 2.0. */ +import type { ReturnTypeFromChainable } from '../../types'; +import { indexEndpointHosts } from '../../tasks/index_endpoint_hosts'; import { login } from '../../tasks/login'; -import { runEndpointLoaderScript } from '../../tasks/run_endpoint_loader'; describe('Endpoints page', () => { + let endpointData: ReturnTypeFromChainable; + before(() => { - runEndpointLoaderScript(); + indexEndpointHosts().then((indexEndpoints) => { + endpointData = indexEndpoints; + }); }); beforeEach(() => { login(); }); + after(() => { + if (endpointData) { + endpointData.cleanup(); + // @ts-expect-error ignore setting to undefined + endpointData = undefined; + } + }); + it('Loads the endpoints page', () => { cy.visit('/app/security/administration/endpoints'); cy.contains('Hosts running Elastic Defend').should('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 new file mode 100644 index 0000000000000..579c0cab8c540 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/isolate.cy.ts @@ -0,0 +1,328 @@ +/* + * 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 { getEndpointListPath } from '../../../common/routing'; +import { + interceptActionRequests, + isolateHostWithComment, + openAlertDetails, + openCaseAlertDetails, + releaseHostWithComment, + sendActionResponse, + waitForReleaseOption, +} from '../../tasks/isolate'; +import type { ActionDetails } from '../../../../../common/endpoint/types'; +import { closeAllToasts } from '../../tasks/close_all_toasts'; +import type { ReturnTypeFromChainable } from '../../types'; +import { addAlertsToCase } from '../../tasks/add_alerts_to_case'; +import { APP_ALERTS_PATH, APP_CASES_PATH, APP_PATH } from '../../../../../common/constants'; +import { login } from '../../tasks/login'; +import { indexNewCase } from '../../tasks/index_new_case'; +import { indexEndpointHosts } from '../../tasks/index_endpoint_hosts'; +import { indexEndpointRuleAlerts } from '../../tasks/index_endpoint_rule_alerts'; + +describe('Isolate command', () => { + describe('from Manage', () => { + let endpointData: ReturnTypeFromChainable; + let isolatedEndpointData: ReturnTypeFromChainable; + + before(() => { + indexEndpointHosts({ + count: 2, + withResponseActions: false, + isolation: false, + }).then((indexEndpoints) => { + endpointData = indexEndpoints; + }); + + indexEndpointHosts({ + count: 2, + withResponseActions: false, + isolation: true, + }).then((indexEndpoints) => { + isolatedEndpointData = indexEndpoints; + }); + }); + + after(() => { + if (endpointData) { + endpointData.cleanup(); + // @ts-expect-error ignore setting to undefined + endpointData = undefined; + } + + if (isolatedEndpointData) { + isolatedEndpointData.cleanup(); + // @ts-expect-error ignore setting to undefined + isolatedEndpointData = undefined; + } + }); + beforeEach(() => { + login(); + }); + 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(); + 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'); + }); + }); + }); + }); + }); + + describe('from Alerts', () => { + let endpointData: ReturnTypeFromChainable; + let alertData: ReturnTypeFromChainable; + let hostname: string; + + before(() => { + indexEndpointHosts({ withResponseActions: false, isolation: false }) + .then((indexEndpoints) => { + endpointData = indexEndpoints; + hostname = endpointData.data.hosts[0].host.name; + }) + .then(() => { + return indexEndpointRuleAlerts({ + endpointAgentId: endpointData.data.hosts[0].agent.id, + endpointHostname: endpointData.data.hosts[0].host.name, + endpointIsolated: false, + }); + }); + }); + + after(() => { + if (endpointData) { + endpointData.cleanup(); + // @ts-expect-error ignore setting to undefined + endpointData = undefined; + } + + if (alertData) { + alertData.cleanup(); + // @ts-expect-error ignore setting to undefined + alertData = undefined; + } + }); + + beforeEach(() => { + login(); + }); + + it('should isolate and release host', () => { + const isolateComment = `Isolating ${hostname}`; + const releaseComment = `Releasing ${hostname}`; + let isolateRequestResponse: ActionDetails; + let releaseRequestResponse: ActionDetails; + + cy.visit(APP_ALERTS_PATH); + closeAllToasts(); + + cy.getByTestSubj('alertsTable').within(() => { + cy.getByTestSubj('expand-event') + .first() + .within(() => { + cy.get(`[data-is-loading="true"]`).should('exist'); + }); + cy.getByTestSubj('expand-event') + .first() + .within(() => { + cy.get(`[data-is-loading="true"]`).should('not.exist'); + }); + }); + + openAlertDetails(); + + isolateHostWithComment(isolateComment, hostname); + + interceptActionRequests((responseBody) => { + isolateRequestResponse = responseBody; + }, 'isolate'); + + cy.getByTestSubj('hostIsolateConfirmButton').click(); + + cy.wait('@isolate').then(() => { + sendActionResponse(isolateRequestResponse); + }); + + cy.contains(`Isolation on host ${hostname} successfully submitted`); + + 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(); + } + }); + + releaseHostWithComment(releaseComment, hostname); + + interceptActionRequests((responseBody) => { + releaseRequestResponse = responseBody; + }, 'release'); + + cy.contains('Confirm').click(); + + cy.wait('@release').then(() => { + sendActionResponse(releaseRequestResponse); + }); + + cy.contains(`Release on host ${hostname} 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 endpointData: ReturnTypeFromChainable; + let caseData: ReturnTypeFromChainable; + let alertData: ReturnTypeFromChainable; + let caseAlertActions: ReturnType; + let alertId: string; + let caseUrlPath: string; + let hostname: string; + + before(() => { + indexNewCase().then((indexCase) => { + caseData = indexCase; + caseUrlPath = `${APP_CASES_PATH}/${indexCase.data.id}`; + }); + + indexEndpointHosts({ withResponseActions: false, isolation: false }) + .then((indexEndpoints) => { + endpointData = indexEndpoints; + hostname = endpointData.data.hosts[0].host.name; + }) + .then(() => { + return indexEndpointRuleAlerts({ + endpointAgentId: endpointData.data.hosts[0].agent.id, + endpointHostname: endpointData.data.hosts[0].host.name, + endpointIsolated: false, + }).then((indexedAlert) => { + alertData = indexedAlert; + alertId = alertData.alerts[0]._id; + }); + }) + .then(() => { + caseAlertActions = addAlertsToCase({ + caseId: caseData.data.id, + alertIds: [alertId], + }); + }); + }); + + after(() => { + if (caseData) { + caseData.cleanup(); + // @ts-expect-error ignore setting to undefined + caseData = undefined; + } + + if (endpointData) { + endpointData.cleanup(); + // @ts-expect-error ignore setting to undefined + endpointData = undefined; + } + + if (alertData) { + alertData.cleanup(); + // @ts-expect-error ignore setting to undefined + alertData = undefined; + } + }); + + beforeEach(() => { + login(); + }); + + it('should isolate and release host', () => { + let isolateRequestResponse: ActionDetails; + let releaseRequestResponse: ActionDetails; + const isolateComment = `Isolating ${hostname}`; + const releaseComment = `Releasing ${hostname}`; + const caseAlertId = caseAlertActions.comments[alertId]; + + cy.visit(caseUrlPath); + closeAllToasts(); + openCaseAlertDetails(caseAlertId); + + isolateHostWithComment(isolateComment, hostname); + + interceptActionRequests((responseBody) => { + isolateRequestResponse = responseBody; + }, 'isolate'); + + cy.getByTestSubj('hostIsolateConfirmButton').click(); + + cy.wait('@isolate').then(() => { + sendActionResponse(isolateRequestResponse); + }); + + cy.contains(`Isolation on host ${hostname} successfully submitted`); + + 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, hostname); + + interceptActionRequests((responseBody) => { + releaseRequestResponse = responseBody; + }, 'release'); + + cy.contains('Confirm').click(); + + cy.wait('@release').then(() => { + sendActionResponse(releaseRequestResponse); + }); + + cy.contains(`Release on host ${hostname} 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/support/data_loaders.ts b/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts index b31ee2ec25874..6dd4bedaa8937 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts @@ -8,12 +8,17 @@ // / import type { CasePostRequest } from '@kbn/cases-plugin/common/api'; +import { sendEndpointActionResponse } from '../../../../scripts/endpoint/agent_emulator/services/endpoint_response_actions'; import type { IndexedEndpointPolicyResponse } from '../../../../common/endpoint/data_loaders/index_endpoint_policy_response'; import { deleteIndexedEndpointPolicyResponse, indexEndpointPolicyResponse, } from '../../../../common/endpoint/data_loaders/index_endpoint_policy_response'; -import type { HostPolicyResponse } from '../../../../common/endpoint/types'; +import type { + ActionDetails, + HostPolicyResponse, + LogsEndpointActionResponse, +} from '../../../../common/endpoint/types'; import type { IndexEndpointHostsCyTaskOptions } from '../types'; import type { IndexedEndpointRuleAlerts, @@ -95,12 +100,14 @@ export const dataLoaders = ( indexEndpointHosts: async (options: IndexEndpointHostsCyTaskOptions = {}) => { const { kbnClient, esClient } = await stackServicesPromise; - const { count: numHosts, version, os } = options; + const { count: numHosts, version, os, isolation, withResponseActions } = options; return cyLoadEndpointDataHandler(esClient, kbnClient, { numHosts, version, os, + isolation, + withResponseActions, }); }, @@ -140,5 +147,13 @@ export const dataLoaders = ( const { esClient } = await stackServicesPromise; return deleteIndexedEndpointPolicyResponse(esClient, indexedData).then(() => null); }, + + sendHostActionResponse: async (data: { + action: ActionDetails; + state: { state?: 'success' | 'failure' }; + }): Promise => { + const { esClient } = await stackServicesPromise; + return sendEndpointActionResponse(esClient, data.action, { state: data.state.state }); + }, }); }; diff --git a/x-pack/plugins/security_solution/public/management/cypress/support/plugin_handlers/endpoint_data_loader.ts b/x-pack/plugins/security_solution/public/management/cypress/support/plugin_handlers/endpoint_data_loader.ts index cfff2e9a2a4b0..9d0f5ac135d5d 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/support/plugin_handlers/endpoint_data_loader.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/support/plugin_handlers/endpoint_data_loader.ts @@ -36,6 +36,9 @@ export interface CyLoadEndpointDataOptions enableFleetIntegration: boolean; generatorSeed: string; waitUntilTransformed: boolean; + withResponseActions: boolean; + isolation: boolean; + bothIsolatedAndNormalEndpoints?: boolean; } /** @@ -58,10 +61,12 @@ export const cyLoadEndpointDataHandler = async ( waitUntilTransformed = true, version = kibanaPackageJson.version, os, + withResponseActions, + isolation, } = options; const DocGenerator = EndpointDocGenerator.custom({ - CustomMetadataGenerator: EndpointMetadataGenerator.custom({ version, os }), + CustomMetadataGenerator: EndpointMetadataGenerator.custom({ version, os, isolation }), }); if (waitUntilTransformed) { @@ -85,7 +90,8 @@ export const cyLoadEndpointDataHandler = async ( alertsPerHost, enableFleetIntegration, undefined, - DocGenerator + DocGenerator, + withResponseActions ); if (waitUntilTransformed) { diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/index_endpoint_rule_alerts.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/index_endpoint_rule_alerts.ts index 498c105d0da49..21ebd75fa6329 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/index_endpoint_rule_alerts.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/index_endpoint_rule_alerts.ts @@ -12,6 +12,8 @@ import type { export const indexEndpointRuleAlerts = (options: { endpointAgentId: string; + endpointHostname?: string; + endpointIsolated?: boolean; count?: number; }): Cypress.Chainable< Pick & { 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 new file mode 100644 index 0000000000000..b00b567852026 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/isolate.ts @@ -0,0 +1,69 @@ +/* + * 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 { ActionDetails } from '../../../../common/endpoint/types'; + +const API_ENDPOINT_ACTION_PATH = '/api/endpoint/action/*'; +export const interceptActionRequests = ( + cb: (responseBody: ActionDetails) => void, + alias: string +): void => { + cy.intercept('POST', API_ENDPOINT_ACTION_PATH, (req) => { + req.continue((res) => { + const { + body: { action, data }, + } = res; + + cb({ action, ...data }); + }); + }).as(alias); +}; + +export const sendActionResponse = (action: ActionDetails): void => { + cy.task('sendHostActionResponse', { + action, + state: { state: 'success' }, + }); +}; + +export const isolateHostWithComment = (comment: string, hostname: string): void => { + cy.getByTestSubj('isolate-host-action-item').click(); + cy.contains(`Isolate host ${hostname} from network.`); + cy.getByTestSubj('endpointHostIsolationForm'); + cy.getByTestSubj('host_isolation_comment').type(comment); +}; + +export const releaseHostWithComment = (comment: string, hostname: string): void => { + cy.contains(`${hostname} is currently isolated.`); + cy.getByTestSubj('endpointHostIsolationForm'); + cy.getByTestSubj('host_isolation_comment').type(comment); +}; + +export const openAlertDetails = (): void => { + cy.getByTestSubj('expand-event').first().click(); + cy.getByTestSubj('take-action-dropdown-btn').click(); +}; + +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) => { + if ($status.find('[title="Isolated"]').length > 0) { + cy.contains('Release host').click(); + } else { + cy.getByTestSubj('euiFlyoutCloseButton').click(); + openCaseAlertDetails(alertId); + cy.getByTestSubj('event-field-agent.status').within(() => { + cy.contains('Isolated'); + }); + cy.contains('Release host').click(); + } + }); +}; diff --git a/x-pack/plugins/security_solution/public/management/cypress/types.ts b/x-pack/plugins/security_solution/public/management/cypress/types.ts index 0741f7fab1ad0..97d635a3b6840 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/types.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/types.ts @@ -7,6 +7,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import type { ActionDetails } from '../../../common/endpoint/types'; import type { CyLoadEndpointDataOptions } from './support/plugin_handlers/endpoint_data_loader'; type PossibleChainable = @@ -41,5 +42,15 @@ export type ReturnTypeFromChainable = C extends Cyp : never; export type IndexEndpointHostsCyTaskOptions = Partial< - { count: number } & Pick + { count: number; withResponseActions: boolean } & Pick< + CyLoadEndpointDataOptions, + 'version' | 'os' | 'isolation' + > >; + +export interface HostActionResponse { + data: { + action: ActionDetails; + state: { state?: 'success' | 'failure' }; + }; +} diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts index b72d4fa30777d..40abffc508fab 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts @@ -256,15 +256,15 @@ describe('endpoint list middleware', () => { query: { agent_ids: [ '0dc3661d-6e67-46b0-af39-6f12b025fcb0', - '34634c58-24b4-4448-80f4-107fb9918494', - '5a1298e3-e607-4bc0-8ef6-6d6a811312f2', - '78c54b13-596d-4891-95f4-80092d04454b', - '445f1fd2-5f81-4ddd-bdb6-f0d1bf2efe90', - 'd77a3fc6-3096-4852-a6ee-f6b09278fbc6', - '892fcccf-1bd8-45a2-a9cc-9a7860a3cb81', - '693a3110-5ba0-4284-a264-5d78301db08c', - '554db084-64fa-4e4a-ba47-2ba713f9932b', - 'c217deb6-674d-4f97-bb1d-a3a04238e6d7', + 'fe16dda9-7f34-434c-9824-b4844880f410', + 'f412728b-929c-48d5-bdb6-5a1298e3e607', + 'd0405ddc-1e7c-48f0-93d7-d55f954bd745', + '46d78dd2-aedf-4d3f-b3a9-da445f1fd25f', + '5aafa558-26b8-4bb4-80e2-ac0644d77a3f', + 'edac2c58-1748-40c3-853c-8fab48c333d7', + '06b7223a-bb2a-428a-9021-f1c0d2267ada', + 'b8daa43b-7f73-4684-9221-dbc8b769405e', + 'fbc06310-7d41-46b8-a5ea-ceed8a993b1a', ], }, }); diff --git a/x-pack/plugins/security_solution/scripts/endpoint/agent_emulator/services/endpoint_response_actions.ts b/x-pack/plugins/security_solution/scripts/endpoint/agent_emulator/services/endpoint_response_actions.ts index 2b3d73efac361..25c2e5f6327be 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/agent_emulator/services/endpoint_response_actions.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/agent_emulator/services/endpoint_response_actions.ts @@ -137,15 +137,21 @@ export const sendEndpointActionResponse = async ( message: 'Endpoint encountered an error and was unable to apply action to host', }; - if (endpointResponse.EndpointActions.data.command === 'get-file') { + if ( + endpointResponse.EndpointActions.data.command === 'get-file' && + endpointResponse.EndpointActions.data.output + ) { ( - endpointResponse.EndpointActions.data.output?.content as ResponseActionGetFileOutputContent + endpointResponse.EndpointActions.data.output.content as ResponseActionGetFileOutputContent ).code = endpointActionGenerator.randomGetFileFailureCode(); } - if (endpointResponse.EndpointActions.data.command === 'execute') { + if ( + endpointResponse.EndpointActions.data.command === 'execute' && + endpointResponse.EndpointActions.data.output + ) { ( - endpointResponse.EndpointActions.data.output?.content as ResponseActionExecuteOutputContent + endpointResponse.EndpointActions.data.output.content as ResponseActionExecuteOutputContent ).stderr = 'execute command timed out'; } } diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts index 5d19932090168..735285753ea23 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts @@ -33,27 +33,17 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'Actions', ], [ - 'Host-dpu1a2r2yi', + 'Host-9qenwrl9ko', 'x', 'x', 'Warning', - 'macOS', - '10.2.17.24, 10.56.215.200,10.254.196.130', - 'x', - 'x', - '', - ], - [ - 'Host-rs9wp4o6l9', - 'x', - 'x', - 'Success', 'Linux', - '10.138.79.131, 10.170.160.154', + '10.56.228.101, 10.201.120.140,10.236.180.146', 'x', 'x', '', ], + ['Host-qw2bti801m', 'x', 'x', 'Failure', 'macOS', '10.244.59.227', 'x', 'x', ''], [ 'Host-u5jy6j0pwb', 'x',