From 2f884d53d8e03659e180fd55b3bade6ffabe27f8 Mon Sep 17 00:00:00 2001 From: "konrad.szwarc" Date: Tue, 4 Apr 2023 09:29:25 +0200 Subject: [PATCH 01/13] avatar aria label --- .../cases/public/components/user_actions/comment/actions.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 && ( From 704886cd9976a1b8aa8f8ba4053c1a607c3cbcda Mon Sep 17 00:00:00 2001 From: "konrad.szwarc" Date: Wed, 5 Apr 2023 17:51:46 +0200 Subject: [PATCH 02/13] isolate command e2e coverage --- .../endpoint_metadata_generator.ts | 4 +- .../endpoint_rule_alert_generator.ts | 2 + .../data_loaders/index_endpoint_hosts.ts | 19 +- .../index_endpoint_rule_alerts.ts | 12 +- .../common/endpoint/generate_data.ts | 13 +- .../common/endpoint/index_data.ts | 13 +- .../cypress/e2e/mocked_data/isolate.cy.ts | 305 ++++++++++++++++++ .../cypress/support/data_loaders.ts | 29 +- .../plugin_handlers/endpoint_data_loader.ts | 20 +- .../cypress/tasks/index_endpoint_hosts.ts | 4 + .../tasks/index_endpoint_rule_alerts.ts | 2 + .../management/cypress/tasks/isolate.ts | 68 ++++ .../services/endpoint_response_actions.ts | 18 +- 13 files changed, 482 insertions(+), 27 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/isolate.cy.ts create mode 100644 x-pack/plugins/security_solution/public/management/cypress/tasks/isolate.ts 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 d377e8ccd7dfd..b50ba42457b36 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,10 +20,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..036a1be3461f4 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, + disableEndpointActionsForHost, }: { numDocs: number; client: Client; @@ -96,6 +98,7 @@ export async function indexEndpointHostDocs({ policyResponseIndex: string; enrollFleet: boolean; generator: EndpointDocGenerator; + disableEndpointActionsForHost: 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 (!disableEndpointActionsForHost) { + // 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 9cda7554f0819..faee348263006 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -338,14 +338,16 @@ export class EndpointDocGenerator extends BaseDataGenerator { * * @param seed either a string to seed the random number generator or a random number generator function * @param MetadataGenerator + * @param endpointIsolated */ constructor( seed: string | seedrandom.prng = Math.random().toString(), - MetadataGenerator: typeof EndpointMetadataGenerator = EndpointMetadataGenerator + MetadataGenerator: typeof EndpointMetadataGenerator = EndpointMetadataGenerator, + endpointIsolated?: boolean ) { super(seed); this.metadataGenerator = new MetadataGenerator(seed); - this.commonInfo = this.createHostData(); + this.commonInfo = this.createHostData(endpointIsolated); } /** @@ -387,9 +389,12 @@ export class EndpointDocGenerator extends BaseDataGenerator { }; } - private createHostData(): CommonHostInfo { + private createHostData(endpointIsolated?: boolean): CommonHostInfo { const { agent, elastic, host, Endpoint } = this.metadataGenerator.generate({ - Endpoint: { policy: { applied: this.randomChoice(APPLIED_POLICIES) } }, + Endpoint: { + policy: { applied: this.randomChoice(APPLIED_POLICIES) }, + state: { isolation: endpointIsolated }, + }, }); 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 24431871c09e6..ee1f7d5339d6e 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,10 @@ export type IndexedHostsAndAlertsResponse = IndexedHostsResponse; * @param fleet * @param options * @param DocGenerator + * @param startTransform + * @param disableEndpointActionsForHost + * @param bothIsolatedAndNormalEndpoints + * @param endpointIsolated */ export async function indexHostsAndAlerts( client: Client, @@ -63,7 +67,10 @@ export async function indexHostsAndAlerts( fleet: boolean, options: TreeOptions = {}, DocGenerator: typeof EndpointDocGenerator = EndpointDocGenerator, - startTransform = true + startTransform = true, + disableEndpointActionsForHost = false, + bothIsolatedAndNormalEndpoints = false, + endpointIsolated?: boolean ): Promise { const random = seedrandom(seed); const epmEndpointPackage = await getEndpointPackageInfo(kbnClient); @@ -101,7 +108,8 @@ export async function indexHostsAndAlerts( await stopMetadataTransforms(client); for (let i = 0; i < numHosts; i++) { - const generator = new DocGenerator(random); + const isolateHost = bothIsolatedAndNormalEndpoints && i % 2 === 0; + const generator = new DocGenerator(random, undefined, isolateHost ? true : endpointIsolated); const indexedHosts = await indexEndpointHostDocs({ numDocs, client, @@ -112,6 +120,7 @@ export async function indexHostsAndAlerts( policyResponseIndex, enrollFleet: fleet, generator, + disableEndpointActionsForHost, }); mergeAndAppendArrays(response, indexedHosts); 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..4d4cc1aa8b64d --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/isolate.cy.ts @@ -0,0 +1,305 @@ +/* + * 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 { + 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_CASES_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; + + before(() => { + indexEndpointHosts({ + count: 4, + disableEndpointActionsForHost: true, + endpointIsolated: false, + bothIsolatedAndNormalEndpoints: true, + }).then((indexEndpoints) => { + endpointData = indexEndpoints; + }); + }); + + after(() => { + if (endpointData) { + endpointData.cleanup(); + // @ts-expect-error ignore setting to undefined + endpointData = undefined; + } + }); + beforeEach(() => { + login(); + }); + it('should allow filtering endpoint by Isolated status', () => { + cy.visit('/app/security/administration/endpoints'); + closeAllToasts(); + cy.getByTestSubj('adminSearchBar') + .click() + .type('united.endpoint.Endpoint.state.isolation: true {enter}'); + 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 alertId: string; + let hostname: string; + + before(() => { + indexEndpointHosts({ disableEndpointActionsForHost: true, endpointIsolated: 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; + }); + }); + }); + + 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/security/alerts'); + closeAllToasts(); + + cy.getByTestSubj('filters-global-container').within(() => { + cy.getByTestSubj('queryInput').click().type(`_id:${alertId} {enter}`); + }); + 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({ disableEndpointActionsForHost: true }) + .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').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').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 8229613289d2b..b64bf543f4632 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,6 +8,8 @@ // / import type { CasePostRequest } from '@kbn/cases-plugin/common/api'; +import type { ActionDetails } from '../../../../common/endpoint/types'; +import { sendEndpointActionResponse } from '../../../../scripts/endpoint/agent_emulator/services/endpoint_response_actions'; import type { IndexedEndpointRuleAlerts, DeletedIndexedEndpointRuleAlerts, @@ -86,10 +88,20 @@ export const dataLoaders = ( return null; }, - indexEndpointHosts: async (options: { count?: number }) => { + indexEndpointHosts: async (options: { + count?: number; + seed?: string; + disableEndpointActionsForHost?: boolean; + endpointIsolated?: boolean; + bothIsolatedAndNormalEndpoints?: boolean; + }) => { const { kbnClient, esClient } = await stackServicesPromise; return cyLoadEndpointDataHandler(esClient, kbnClient, { numHosts: options.count, + generatorSeed: options.seed, + disableEndpointActionsForHost: options.disableEndpointActionsForHost, + endpointIsolated: options.endpointIsolated, + bothIsolatedAndNormalEndpoints: options.bothIsolatedAndNormalEndpoints, }); }, @@ -98,7 +110,12 @@ export const dataLoaders = ( return deleteIndexedHostsAndAlerts(esClient, kbnClient, indexedData); }, - indexEndpointRuleAlerts: async (options: { endpointAgentId: string; count?: number }) => { + indexEndpointRuleAlerts: async (options: { + endpointAgentId: string; + endpointHostname?: string; + endpointIsolated?: boolean; + count?: number; + }) => { const { esClient, log } = await stackServicesPromise; return ( await indexEndpointRuleAlerts({ @@ -115,5 +132,13 @@ export const dataLoaders = ( const { esClient, log } = await stackServicesPromise; return deleteIndexedEndpointRuleAlerts(esClient, data, log); }, + + sendHostActionResponse: async (data: { + action: ActionDetails; + state: { state?: 'success' | 'failure' }; + }) => { + 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 55ab851df6503..df2832ef78750 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 @@ -38,6 +38,9 @@ interface CyLoadEndpointDataOptions { generatorSeed: string; waitUntilTransformed: boolean; customIndexFn: () => Promise; + disableEndpointActionsForHost?: boolean; + endpointIsolated?: boolean; + bothIsolatedAndNormalEndpoints?: boolean; } /** @@ -59,6 +62,9 @@ export const cyLoadEndpointDataHandler = async ( generatorSeed = `cy.${Math.random()}`, waitUntilTransformed = true, customIndexFn, + disableEndpointActionsForHost, + bothIsolatedAndNormalEndpoints, + endpointIsolated, } = options; if (waitUntilTransformed) { @@ -84,7 +90,11 @@ export const cyLoadEndpointDataHandler = async ( alertsPerHost, enableFleetIntegration, undefined, - CurrentKibanaVersionDocGenerator + CurrentKibanaVersionDocGenerator, + true, + disableEndpointActionsForHost, + bothIsolatedAndNormalEndpoints, + endpointIsolated ); if (waitUntilTransformed) { @@ -106,14 +116,18 @@ export const cyLoadEndpointDataHandler = async ( // Document Generator override that uses a custom Endpoint Metadata generator and sets the // `agent.version` to the current version const CurrentKibanaVersionDocGenerator = class extends EndpointDocGenerator { - constructor(seedValue: string | seedrandom.prng) { + constructor( + seedValue: string | seedrandom.prng, + metadataGenerator?: typeof EndpointMetadataGenerator, + endpointIsolated?: boolean + ) { const MetadataGenerator = class extends EndpointMetadataGenerator { protected randomVersion(): string { return kibanaPackageJson.version; } }; - super(seedValue, MetadataGenerator); + super(seedValue, MetadataGenerator, endpointIsolated); } }; diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/index_endpoint_hosts.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/index_endpoint_hosts.ts index 0821764bf860b..b12c16821de12 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/index_endpoint_hosts.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/index_endpoint_hosts.ts @@ -18,6 +18,10 @@ interface CyIndexEndpointHosts { export const indexEndpointHosts = ( options: { count?: number; + seed?: string; + disableEndpointActionsForHost?: boolean; + endpointIsolated?: boolean; + bothIsolatedAndNormalEndpoints?: boolean; } = {} ): Cypress.Chainable => { return cy.task('indexEndpointHosts', options, { timeout: 120000 }).then((indexHosts) => { 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..f3f7bfb22d81d --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/isolate.ts @@ -0,0 +1,68 @@ +/* + * 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'; + +export const interceptActionRequests = ( + cb: (responseBody: ActionDetails) => void, + alias: string +) => { + cy.intercept('POST', '/api/endpoint/action/*', (req) => { + req.continue((res) => { + const { + body: { action, data }, + } = res; + + cb({ action, ...data }); + }); + }).as(alias); +}; + +export const sendActionResponse = (action: ActionDetails) => { + cy.task('sendHostActionResponse', { + action, + state: { state: 'success' }, + }); +}; + +export const isolateHostWithComment = (comment: string, hostname: string) => { + 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) => { + cy.contains(`${hostname} is currently isolated.`); + cy.getByTestSubj('endpointHostIsolationForm'); + cy.getByTestSubj('host_isolation_comment').type(comment); +}; + +export const openAlertDetails = () => { + cy.getByTestSubj('expand-event').click(); + cy.getByTestSubj('take-action-dropdown-btn').click(); +}; + +export const openCaseAlertDetails = (alertId: string) => { + cy.getByTestSubj(`comment-action-show-alert-${alertId}`).click(); + cy.getByTestSubj('take-action-dropdown-btn').click(); +}; +export const waitForReleaseOption = (alertId: string) => { + 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/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 5547f74bc0989..c9ac5554ff910 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 @@ -40,8 +40,6 @@ const ES_INDEX_OPTIONS = { headers: { 'X-elastic-product-origin': 'fleet' } }; export const fleetActionGenerator = new FleetActionGenerator(); -export const endpointActionGenerator = new EndpointActionGenerator(); - export const sleep = (ms: number = 1000) => new Promise((r) => setTimeout(r, ms)); export const fetchEndpointActionList = async ( @@ -118,6 +116,7 @@ export const sendEndpointActionResponse = async ( action: ActionDetails, { state }: { state?: 'success' | 'failure' } = {} ): Promise => { + const endpointActionGenerator = new EndpointActionGenerator(); const endpointResponse = endpointActionGenerator.generateResponse({ agent: { id: action.agents[0] }, EndpointActions: { @@ -137,15 +136,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'; } } @@ -282,6 +287,7 @@ type ResponseOutput = Pick< >; const getOutputDataIfNeeded = (action: ActionDetails): ResponseOutput => { + const endpointActionGenerator = new EndpointActionGenerator(); switch (action.command) { case 'running-processes': return { From 5db6e07891c2829cb9bdeae3509ad1f79384af6c Mon Sep 17 00:00:00 2001 From: "konrad.szwarc" Date: Thu, 6 Apr 2023 09:12:04 +0200 Subject: [PATCH 03/13] typings --- .../common/endpoint/data_loaders/index_endpoint_hosts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 036a1be3461f4..7693653c3c5a6 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 @@ -98,7 +98,7 @@ export async function indexEndpointHostDocs({ policyResponseIndex: string; enrollFleet: boolean; generator: EndpointDocGenerator; - disableEndpointActionsForHost: boolean; + disableEndpointActionsForHost?: boolean; }): Promise { const timeBetweenDocs = 6 * 3600 * 1000; // 6 hours between metadata documents const timestamp = new Date().getTime(); From 4de9906546a4b010e5527d4c060289f4c774cd3c Mon Sep 17 00:00:00 2001 From: "konrad.szwarc" Date: Thu, 6 Apr 2023 09:52:35 +0200 Subject: [PATCH 04/13] typings --- .../cypress/support/plugin_handlers/endpoint_data_loader.ts | 1 - 1 file changed, 1 deletion(-) 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 df2832ef78750..d109d5f68bfd3 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 @@ -91,7 +91,6 @@ export const cyLoadEndpointDataHandler = async ( enableFleetIntegration, undefined, CurrentKibanaVersionDocGenerator, - true, disableEndpointActionsForHost, bothIsolatedAndNormalEndpoints, endpointIsolated From 67eb92cc239781ce417b70d7d6d28c331334f22a Mon Sep 17 00:00:00 2001 From: "konrad.szwarc" Date: Mon, 17 Apr 2023 15:38:40 +0200 Subject: [PATCH 05/13] cleanup --- .../cypress/e2e/mocked_data/isolate.cy.ts | 21 +++++++++++++------ .../management/cypress/tasks/isolate.ts | 2 +- 2 files changed, 16 insertions(+), 7 deletions(-) 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 4d4cc1aa8b64d..e3338586b56fb 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 @@ -68,7 +68,6 @@ describe('Isolate command', () => { describe('from Alerts', () => { let endpointData: ReturnTypeFromChainable; let alertData: ReturnTypeFromChainable; - let alertId: string; let hostname: string; before(() => { @@ -82,9 +81,6 @@ describe('Isolate command', () => { 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; }); }); }); @@ -116,9 +112,22 @@ describe('Isolate command', () => { cy.visit('/app/security/alerts'); closeAllToasts(); - cy.getByTestSubj('filters-global-container').within(() => { - cy.getByTestSubj('queryInput').click().type(`_id:${alertId} {enter}`); + // cy.getByTestSubj('filters-global-container').within(() => { + // cy.getByTestSubj('queryInput').click().type(`_id:${alertId} {enter}`); + // }); + 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); 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 f3f7bfb22d81d..e85b87e88fa8d 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 @@ -43,7 +43,7 @@ export const releaseHostWithComment = (comment: string, hostname: string) => { }; export const openAlertDetails = () => { - cy.getByTestSubj('expand-event').click(); + cy.getByTestSubj('expand-event').first().click(); cy.getByTestSubj('take-action-dropdown-btn').click(); }; From fcc702f4a4923d74ec56af11830c1d241b3f6b6b Mon Sep 17 00:00:00 2001 From: "konrad.szwarc" Date: Mon, 17 Apr 2023 18:19:45 +0200 Subject: [PATCH 06/13] use custom document generator --- .../endpoint_metadata_generator.ts | 5 ++++ .../data_loaders/index_endpoint_hosts.ts | 6 ++-- .../common/endpoint/generate_data.ts | 9 ++---- .../common/endpoint/index_data.ts | 13 +++----- .../public/management/cypress/cypress.d.ts | 13 ++++++-- .../cypress/e2e/mocked_data/isolate.cy.ts | 30 ++++++++++++++----- .../cypress/support/data_loaders.ts | 19 ++++++++++-- .../plugin_handlers/endpoint_data_loader.ts | 15 ++++------ .../public/management/cypress/types.ts | 13 +++++++- .../endpoint_hosts/store/middleware.test.ts | 18 +++++------ .../services/endpoint_response_actions.ts | 3 +- .../apps/endpoint/endpoint_list.ts | 16 ++-------- 12 files changed, 97 insertions(+), 63 deletions(-) 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 8b6b11ac9260e..9910477437e82 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); } 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 7693653c3c5a6..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 @@ -87,7 +87,7 @@ export async function indexEndpointHostDocs({ policyResponseIndex, enrollFleet, generator, - disableEndpointActionsForHost, + withResponseActions = true, }: { numDocs: number; client: Client; @@ -98,7 +98,7 @@ export async function indexEndpointHostDocs({ policyResponseIndex: string; enrollFleet: boolean; generator: EndpointDocGenerator; - disableEndpointActionsForHost?: boolean; + withResponseActions?: boolean; }): Promise { const timeBetweenDocs = 6 * 3600 * 1000; // 6 hours between metadata documents const timestamp = new Date().getTime(); @@ -193,7 +193,7 @@ export async function indexEndpointHostDocs({ }, }; - if (!disableEndpointActionsForHost) { + if (withResponseActions) { // Create some fleet endpoint actions and .logs-endpoint actions for this Host const actionsResponse = await indexEndpointAndFleetActionsForHost( client, 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 6285c0b6f4309..76ee903eb6889 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -340,16 +340,14 @@ export class EndpointDocGenerator extends BaseDataGenerator { * * @param seed either a string to seed the random number generator or a random number generator function * @param MetadataGenerator - * @param endpointIsolated */ constructor( seed: string | seedrandom.prng = Math.random().toString(), - MetadataGenerator: typeof EndpointMetadataGenerator = EndpointMetadataGenerator, - endpointIsolated?: boolean + MetadataGenerator: typeof EndpointMetadataGenerator = EndpointMetadataGenerator ) { super(seed); this.metadataGenerator = new MetadataGenerator(seed); - this.commonInfo = this.createHostData(endpointIsolated); + this.commonInfo = this.createHostData(); } /** @@ -410,11 +408,10 @@ export class EndpointDocGenerator extends BaseDataGenerator { }; } - private createHostData(endpointIsolated?: boolean): CommonHostInfo { + private createHostData(): CommonHostInfo { const { agent, elastic, host, Endpoint } = this.metadataGenerator.generate({ Endpoint: { policy: { applied: this.randomChoice(APPLIED_POLICIES) }, - state: { isolation: endpointIsolated }, }, }); 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 9b8181036521b..8241c1f0f1e10 100644 --- a/x-pack/plugins/security_solution/common/endpoint/index_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/index_data.ts @@ -48,9 +48,7 @@ export type IndexedHostsAndAlertsResponse = IndexedHostsResponse; * @param fleet * @param options * @param DocGenerator - * @param disableEndpointActionsForHost - * @param bothIsolatedAndNormalEndpoints - * @param endpointIsolated + * @param withResponseActions */ export async function indexHostsAndAlerts( client: Client, @@ -66,9 +64,7 @@ export async function indexHostsAndAlerts( fleet: boolean, options: TreeOptions = {}, DocGenerator: typeof EndpointDocGenerator = EndpointDocGenerator, - disableEndpointActionsForHost = false, - bothIsolatedAndNormalEndpoints = false, - endpointIsolated?: boolean + withResponseActions = true ): Promise { const random = seedrandom(seed); const epmEndpointPackage = await getEndpointPackageInfo(kbnClient); @@ -109,8 +105,7 @@ export async function indexHostsAndAlerts( } for (let i = 0; i < numHosts; i++) { - const isolateHost = bothIsolatedAndNormalEndpoints && i % 2 === 0; - const generator = new DocGenerator(random, undefined, isolateHost ? true : endpointIsolated); + const generator = new DocGenerator(random, undefined); const indexedHosts = await indexEndpointHostDocs({ numDocs, client, @@ -121,7 +116,7 @@ export async function indexHostsAndAlerts( policyResponseIndex, enrollFleet: fleet, generator, - disableEndpointActionsForHost, + 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/isolate.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/isolate.cy.ts index e3338586b56fb..3cc2d03854c06 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 @@ -27,16 +27,24 @@ import { indexEndpointRuleAlerts } from '../../tasks/index_endpoint_rule_alerts' describe('Isolate command', () => { describe('from Manage', () => { let endpointData: ReturnTypeFromChainable; + let isolatedEndpointData: ReturnTypeFromChainable; before(() => { indexEndpointHosts({ - count: 4, - disableEndpointActionsForHost: true, - endpointIsolated: false, - bothIsolatedAndNormalEndpoints: true, + count: 2, + withResponseActions: false, + isolation: false, }).then((indexEndpoints) => { endpointData = indexEndpoints; }); + + indexEndpointHosts({ + count: 2, + withResponseActions: false, + isolation: true, + }).then((indexEndpoints) => { + isolatedEndpointData = indexEndpoints; + }); }); after(() => { @@ -45,6 +53,12 @@ describe('Isolate command', () => { // @ts-expect-error ignore setting to undefined endpointData = undefined; } + + if (isolatedEndpointData) { + isolatedEndpointData.cleanup(); + // @ts-expect-error ignore setting to undefined + isolatedEndpointData = undefined; + } }); beforeEach(() => { login(); @@ -71,7 +85,7 @@ describe('Isolate command', () => { let hostname: string; before(() => { - indexEndpointHosts({ disableEndpointActionsForHost: true, endpointIsolated: false }) + indexEndpointHosts({ withResponseActions: false, isolation: false }) .then((indexEndpoints) => { endpointData = indexEndpoints; hostname = endpointData.data.hosts[0].host.name; @@ -196,7 +210,7 @@ describe('Isolate command', () => { caseUrlPath = `${APP_CASES_PATH}/${indexCase.data.id}`; }); - indexEndpointHosts({ disableEndpointActionsForHost: true }) + indexEndpointHosts({ withResponseActions: false, isolation: false }) .then((indexEndpoints) => { endpointData = indexEndpoints; hostname = endpointData.data.hosts[0].host.name; @@ -270,7 +284,7 @@ describe('Isolate command', () => { cy.getByTestSubj('euiFlyoutCloseButton').click(); - cy.getByTestSubj('user-actions').within(() => { + cy.getByTestSubj('user-actions-list').within(() => { cy.contains(isolateComment); cy.get('[aria-label="lock"]').should('exist'); cy.get('[aria-label="lockOpen"]').should('not.exist'); @@ -293,7 +307,7 @@ describe('Isolate command', () => { cy.contains(`Release on host ${hostname} successfully submitted`); cy.getByTestSubj('euiFlyoutCloseButton').click(); - cy.getByTestSubj('user-actions').within(() => { + cy.getByTestSubj('user-actions-list').within(() => { cy.contains(releaseComment); cy.contains(isolateComment); cy.get('[aria-label="lock"]').should('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 dd23898263d17..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,8 +36,8 @@ export interface CyLoadEndpointDataOptions enableFleetIntegration: boolean; generatorSeed: string; waitUntilTransformed: boolean; - disableEndpointActionsForHost?: boolean; - endpointIsolated?: boolean; + withResponseActions: boolean; + isolation: boolean; bothIsolatedAndNormalEndpoints?: boolean; } @@ -61,13 +61,12 @@ export const cyLoadEndpointDataHandler = async ( waitUntilTransformed = true, version = kibanaPackageJson.version, os, - disableEndpointActionsForHost, - bothIsolatedAndNormalEndpoints, - endpointIsolated, + withResponseActions, + isolation, } = options; const DocGenerator = EndpointDocGenerator.custom({ - CustomMetadataGenerator: EndpointMetadataGenerator.custom({ version, os }), + CustomMetadataGenerator: EndpointMetadataGenerator.custom({ version, os, isolation }), }); if (waitUntilTransformed) { @@ -92,9 +91,7 @@ export const cyLoadEndpointDataHandler = async ( enableFleetIntegration, undefined, DocGenerator, - disableEndpointActionsForHost, - bothIsolatedAndNormalEndpoints, - endpointIsolated + withResponseActions ); if (waitUntilTransformed) { 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 615cbe38baf7f..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 @@ -40,6 +40,8 @@ const ES_INDEX_OPTIONS = { headers: { 'X-elastic-product-origin': 'fleet' } }; export const fleetActionGenerator = new FleetActionGenerator(); +export const endpointActionGenerator = new EndpointActionGenerator(); + export const sleep = (ms: number = 1000) => new Promise((r) => setTimeout(r, ms)); export const fetchEndpointActionList = async ( @@ -116,7 +118,6 @@ export const sendEndpointActionResponse = async ( action: ActionDetails, { state }: { state?: 'success' | 'failure' } = {} ): Promise => { - const endpointActionGenerator = new EndpointActionGenerator(); const endpointResponse = endpointActionGenerator.generateResponse({ agent: { id: action.agents[0] }, EndpointActions: { 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 0bfb0cc5bba1d..2efd2bf52309b 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', 'Linux', - '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', 'Linux', '10.244.59.227', 'x', 'x', ''], [ 'Host-u5jy6j0pwb', 'x', From c54509bff360ef7b90f208ae333dc8369faf331e Mon Sep 17 00:00:00 2001 From: "konrad.szwarc" Date: Tue, 18 Apr 2023 15:06:37 +0200 Subject: [PATCH 07/13] manualy refresh result list --- .../public/management/cypress/e2e/mocked_data/isolate.cy.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 3cc2d03854c06..058e8e1169669 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 @@ -68,7 +68,9 @@ describe('Isolate command', () => { closeAllToasts(); cy.getByTestSubj('adminSearchBar') .click() - .type('united.endpoint.Endpoint.state.isolation: true {enter}'); + .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(() => { From 9415935acdf6647991926214adac95fec77f93cc Mon Sep 17 00:00:00 2001 From: "konrad.szwarc" Date: Tue, 18 Apr 2023 16:59:47 +0200 Subject: [PATCH 08/13] remove artifacts after endpoints.cy.ts test --- .../cypress/e2e/mocked_data/endpoints.cy.ts | 17 +++++++++++++++-- .../apps/endpoint/endpoint_list.ts | 16 +++------------- 2 files changed, 18 insertions(+), 15 deletions(-) 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/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', From 193968bf3c374794d4a71ea11f304a375f93eedc Mon Sep 17 00:00:00 2001 From: "konrad.szwarc" Date: Thu, 20 Apr 2023 12:08:01 +0200 Subject: [PATCH 09/13] backport isolate e2e tests to multipass --- .../cypress/e2e/endpoint/isolate.cy.ts | 286 ++++++++++++++++++ .../cypress/e2e/mocked_data/isolate.cy.ts | 30 +- .../management/cypress/tasks/api_fixtures.ts | 45 ++- .../management/cypress/tasks/isolate.ts | 71 +++++ .../endpoint_agent_runner/elastic_endpoint.ts | 2 +- 5 files changed, 400 insertions(+), 34 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/isolate.cy.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 new file mode 100644 index 0000000000000..53e6932da147f --- /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 } 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/security/administration/endpoints'); + 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('rowIsolationStatus').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/security/administration/endpoints'); + 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/security/administration/endpoints'); + 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 058e8e1169669..4a3922f9cdffe 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 @@ -6,6 +6,9 @@ */ import { + checkEndpointListForIsolatedHosts, + checkFlyoutEndpointIsolation, + filterOutIsolatedHosts, interceptActionRequests, isolateHostWithComment, openAlertDetails, @@ -66,18 +69,9 @@ describe('Isolate command', () => { it('should allow filtering endpoint by Isolated status', () => { cy.visit('/app/security/administration/endpoints'); 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(); }); }); @@ -163,18 +157,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 e85b87e88fa8d..6d20059488b82 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'; export const interceptActionRequests = ( @@ -66,3 +67,73 @@ export const waitForReleaseOption = (alertId: string) => { } }); }; + +export const visitRuleAlerts = (ruleName: string) => { + cy.visit('/app/security/rules'); + cy.contains(ruleName).click(); +}; +export const checkFlyoutEndpointIsolation = () => { + 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) => { + 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) => { + 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 = () => { + cy.getByTestSubj('adminSearchBar').click().type('united.endpoint.Endpoint.state.isolation: true'); + cy.getByTestSubj('querySubmitButton').click(); +}; + +export const checkEndpointListForIsolatedHosts = (expectIsolated = true) => { + 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) { From b3a7460ec2fbd322bb5903a5098f7b4c42458f86 Mon Sep 17 00:00:00 2001 From: "konrad.szwarc" Date: Thu, 20 Apr 2023 12:13:46 +0200 Subject: [PATCH 10/13] cleanup --- .../public/management/cypress/e2e/mocked_data/isolate.cy.ts | 3 --- 1 file changed, 3 deletions(-) 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 058e8e1169669..c06a839bd7d6f 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 @@ -128,9 +128,6 @@ describe('Isolate command', () => { cy.visit('/app/security/alerts'); closeAllToasts(); - // cy.getByTestSubj('filters-global-container').within(() => { - // cy.getByTestSubj('queryInput').click().type(`_id:${alertId} {enter}`); - // }); cy.getByTestSubj('alertsTable').within(() => { cy.getByTestSubj('expand-event') .first() From b5b6941d104a998bdc4e5cbbe6f85b81489d41a6 Mon Sep 17 00:00:00 2001 From: "konrad.szwarc" Date: Fri, 21 Apr 2023 11:30:22 +0200 Subject: [PATCH 11/13] tweaks --- .../security_solution/common/endpoint/index_data.ts | 2 +- .../management/cypress/e2e/mocked_data/isolate.cy.ts | 7 ++++--- .../public/management/cypress/tasks/isolate.ts | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) 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 8241c1f0f1e10..db5039e5a72f0 100644 --- a/x-pack/plugins/security_solution/common/endpoint/index_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/index_data.ts @@ -105,7 +105,7 @@ export async function indexHostsAndAlerts( } for (let i = 0; i < numHosts; i++) { - const generator = new DocGenerator(random, undefined); + const generator = new DocGenerator(random); const indexedHosts = await indexEndpointHostDocs({ numDocs, client, 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 c06a839bd7d6f..579c0cab8c540 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 @@ -5,6 +5,7 @@ * 2.0. */ +import { getEndpointListPath } from '../../../common/routing'; import { interceptActionRequests, isolateHostWithComment, @@ -18,7 +19,7 @@ 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_CASES_PATH } from '../../../../../common/constants'; +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'; @@ -64,7 +65,7 @@ describe('Isolate command', () => { login(); }); it('should allow filtering endpoint by Isolated status', () => { - cy.visit('/app/security/administration/endpoints'); + cy.visit(APP_PATH + getEndpointListPath({ name: 'endpointList' })); closeAllToasts(); cy.getByTestSubj('adminSearchBar') .click() @@ -125,7 +126,7 @@ describe('Isolate command', () => { let isolateRequestResponse: ActionDetails; let releaseRequestResponse: ActionDetails; - cy.visit('/app/security/alerts'); + cy.visit(APP_ALERTS_PATH); closeAllToasts(); cy.getByTestSubj('alertsTable').within(() => { 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 e85b87e88fa8d..85cb005ae5e73 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 @@ -7,11 +7,12 @@ import type { ActionDetails } from '../../../../common/endpoint/types'; +const API_ENDPOINT_ACTION_PATH = '/api/endpoint/action/*'; export const interceptActionRequests = ( cb: (responseBody: ActionDetails) => void, alias: string ) => { - cy.intercept('POST', '/api/endpoint/action/*', (req) => { + cy.intercept('POST', API_ENDPOINT_ACTION_PATH, (req) => { req.continue((res) => { const { body: { action, data }, From 7c13a7ee32c9d408cfb56d0535f907b21891c209 Mon Sep 17 00:00:00 2001 From: "konrad.szwarc" Date: Fri, 21 Apr 2023 11:59:56 +0200 Subject: [PATCH 12/13] cleanup --- .../public/management/cypress/e2e/endpoint/isolate.cy.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 53e6932da147f..12a974776a38c 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 @@ -6,7 +6,7 @@ */ import type { Agent } from '@kbn/fleet-plugin/common'; -import { APP_CASES_PATH } from '../../../../../common/constants'; +import { APP_CASES_PATH, APP_ENDPOINTS_PATH } from '../../../../../common/constants'; import { closeAllToasts } from '../../tasks/close_all_toasts'; import { checkEndpointListForIsolatedHosts, @@ -67,7 +67,7 @@ describe('Isolate command', () => { }); it('should allow filtering endpoint by Isolated status', () => { - cy.visit('/app/security/administration/endpoints'); + cy.visit(APP_ENDPOINTS_PATH); closeAllToasts(); checkEndpointListForIsolatedHosts(false); @@ -135,7 +135,7 @@ describe('Isolate command', () => { }); it('should have generated endpoint and rule', () => { - cy.visit('/app/security/administration/endpoints'); + cy.visit(APP_ENDPOINTS_PATH); cy.contains(endpointHostname).should('exist'); toggleRuleOffAndOn(ruleName); @@ -219,7 +219,7 @@ describe('Isolate command', () => { }); it('should have generated endpoint and rule', () => { - cy.visit('/app/security/administration/endpoints'); + cy.visit(APP_ENDPOINTS_PATH); cy.contains(endpointHostname).should('exist'); toggleRuleOffAndOn(ruleName); From 2b0757451204aef7486c8d8d071fd841dfeb78b0 Mon Sep 17 00:00:00 2001 From: "konrad.szwarc" Date: Mon, 24 Apr 2023 14:57:54 +0200 Subject: [PATCH 13/13] type returns of helper functions --- .../management/cypress/e2e/endpoint/isolate.cy.ts | 2 +- .../public/management/cypress/tasks/isolate.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) 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 12a974776a38c..db486a6478e1a 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 @@ -84,7 +84,7 @@ describe('Isolate command', () => { cy.getByTestSubj('hostIsolateConfirmButton').click(); cy.contains(`Isolation on host ${endpointHostname} successfully submitted`); cy.getByTestSubj('euiFlyoutCloseButton').click(); - cy.getByTestSubj('rowIsolationStatus').should('contain.text', 'Isolated'); + cy.getByTestSubj('rowHostStatus-actionStatuses').should('contain.text', 'Isolated'); filterOutIsolatedHosts(); checkEndpointListForIsolatedHosts(); 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 6c400da0b5888..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 @@ -74,7 +74,7 @@ export const visitRuleAlerts = (ruleName: string) => { cy.visit('/app/security/rules'); cy.contains(ruleName).click(); }; -export const checkFlyoutEndpointIsolation = () => { +export const checkFlyoutEndpointIsolation = (): void => { cy.getByTestSubj('event-field-agent.status').then(($status) => { if ($status.find('[title="Isolated"]').length > 0) { cy.contains('Release host').click(); @@ -90,7 +90,7 @@ export const checkFlyoutEndpointIsolation = () => { }); }; -export const toggleRuleOffAndOn = (ruleName: string) => { +export const toggleRuleOffAndOn = (ruleName: string): void => { cy.visit('/app/security/rules'); cy.wait(2000); cy.contains(ruleName) @@ -104,7 +104,7 @@ export const toggleRuleOffAndOn = (ruleName: string) => { }); }; -export const filterOutEndpoints = (endpointHostname: string) => { +export const filterOutEndpoints = (endpointHostname: string): void => { cy.getByTestSubj('filters-global-container').within(() => { cy.getByTestSubj('queryInput').click().type(`host.hostname : "${endpointHostname}"`); cy.getByTestSubj('querySubmitButton').click(); @@ -124,12 +124,12 @@ export const createAgentPolicyTask = ( }).then(cb); }; -export const filterOutIsolatedHosts = () => { +export const filterOutIsolatedHosts = (): void => { cy.getByTestSubj('adminSearchBar').click().type('united.endpoint.Endpoint.state.isolation: true'); cy.getByTestSubj('querySubmitButton').click(); }; -export const checkEndpointListForIsolatedHosts = (expectIsolated = true) => { +export const checkEndpointListForIsolatedHosts = (expectIsolated = true): void => { const chainer = expectIsolated ? 'contain.text' : 'not.contain.text'; cy.getByTestSubj('endpointListTable').within(() => { cy.get('tbody tr').each(($tr) => {