From 2f884d53d8e03659e180fd55b3bade6ffabe27f8 Mon Sep 17 00:00:00 2001 From: "konrad.szwarc" Date: Tue, 4 Apr 2023 09:29:25 +0200 Subject: [PATCH 01/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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 b3a7460ec2fbd322bb5903a5098f7b4c42458f86 Mon Sep 17 00:00:00 2001 From: "konrad.szwarc" Date: Thu, 20 Apr 2023 12:13:46 +0200 Subject: [PATCH 09/11] 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 10/11] 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 391ed8c3d0596d5287ba90386b36d9d16e75bd4f Mon Sep 17 00:00:00 2001 From: "konrad.szwarc" Date: Fri, 21 Apr 2023 16:56:11 +0200 Subject: [PATCH 11/11] type returns of helper functions --- .../public/management/cypress/tasks/isolate.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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 85cb005ae5e73..b00b567852026 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 @@ -11,7 +11,7 @@ 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 { @@ -23,36 +23,36 @@ export const interceptActionRequests = ( }).as(alias); }; -export const sendActionResponse = (action: ActionDetails) => { +export const sendActionResponse = (action: ActionDetails): void => { cy.task('sendHostActionResponse', { action, state: { state: 'success' }, }); }; -export const isolateHostWithComment = (comment: string, hostname: string) => { +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) => { +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 = () => { +export const openAlertDetails = (): void => { cy.getByTestSubj('expand-event').first().click(); cy.getByTestSubj('take-action-dropdown-btn').click(); }; -export const openCaseAlertDetails = (alertId: string) => { +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) => { +export const waitForReleaseOption = (alertId: string): void => { openCaseAlertDetails(alertId); cy.getByTestSubj('event-field-agent.status').then(($status) => { if ($status.find('[title="Isolated"]').length > 0) {