From 8c95dd8603781175b3729a534cc82c25357e4c0d Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Mon, 17 Apr 2023 17:28:40 -0400 Subject: [PATCH 01/24] initial setup for a standalone service to create endpoint host VMs --- .../e2e/endpoint/endpoint_alerts.cy.ts | 10 ++ .../endpoint/common/endpoint_host_services.ts | 133 ++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts create mode 100644 x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_host_services.ts diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts new file mode 100644 index 0000000000000..478cbd7c2590e --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts @@ -0,0 +1,10 @@ +/* + * 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. + */ + +describe('Endpoint generated alerts', () => { + // FIXME:PT implement +}); diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_host_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_host_services.ts new file mode 100644 index 0000000000000..6fb643317ef72 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_host_services.ts @@ -0,0 +1,133 @@ +/* + * 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 { KbnClient } from '@kbn/test'; +import type { ToolingLog } from '@kbn/tooling-log'; +import execa from 'execa'; +import { waitForHostToEnroll } from './fleet_services'; + +export interface CreateAndEnrollEndpointHostOptions + extends Pick { + kbnClient: KbnClient; + log: ToolingLog; + /** The fleet Agent Policy ID to use for enrolling the agent */ + agentPolicyId: string; + /** version of the Agent to install. Defaults to stack version */ + version?: string; + /** The name for the host. Will also be the name of the VM */ + hostname?: string; +} + +interface CreateAndEnrollEndpointHostResponse { + /** The virtualization software used to create the new VM */ + provider: 'multipass'; + hostname: string; + agentId: string; +} + +/** + * Creates a new virtual machine (host) and enrolls that with Fleet + */ +export const createAndEnrollEndpointHost = async ( + options: CreateAndEnrollEndpointHostOptions +): Promise => { + // FIXME:PT implement +}; + +interface CreateMultipassVmOptions { + vmName: string; + /** Number of CPUs */ + cpus?: number; + /** Disk size */ + disk?: string; + /** Amount of memory */ + memory?: string; +} + +interface CreateMultipassVmResponse { + vmName: string; +} + +/** + * Creates a new VM using `multipass` + */ +const createMultipassVm = async ({ + vmName, + disk = '8G', + cpus = 1, + memory = '1G', +}: CreateMultipassVmOptions): Promise => { + await execa.command( + `multipass launch --name ${vmName} --disk ${disk} --cpus ${cpus} --memory ${memory}` + ); + + return { + vmName, + }; +}; + +interface EnrollHostWithFleetOptions { + kbnClient: KbnClient; + log: ToolingLog; + vmName: string; + agentDownloadUrl: string; + fleetServerUrl: string; + enrollmentToken: string; +} + +const enrollHostWithFleet = async ({ + kbnClient, + log, + vmName, + fleetServerUrl, + agentDownloadUrl, + enrollmentToken, +}: EnrollHostWithFleetOptions): Promise => { + const agentDownloadedFile = agentDownloadUrl.substring(agentDownloadUrl.lastIndexOf('/') + 1); + const vmDirName = agentDownloadedFile.replace(/\.tar\.gz$/, ''); + + await execa.command( + `multipass exec ${vmName} -- curl -L ${agentDownloadUrl} -o ${agentDownloadedFile}` + ); + await execa.command(`multipass exec ${vmName} -- tar -zxf ${agentDownloadedFile}`); + await execa.command(`multipass exec ${vmName} -- rm -f ${agentDownloadedFile}`); + + const agentInstallArguments = [ + 'exec', + + vmName, + + '--working-directory', + `/home/ubuntu/${vmDirName}`, + + '--', + + 'sudo', + + './elastic-agent', + + 'install', + + '--insecure', + + '--force', + + '--url', + fleetServerUrl, + + '--enrollment-token', + enrollmentToken, + ]; + + log.info(`Enrolling elastic agent with Fleet`); + log.verbose(`Command: multipass ${agentInstallArguments.join(' ')}`); + + await execa(`multipass`, agentInstallArguments); + + log.info(`Waiting for Agent to check-in with Fleet`); + await waitForHostToEnroll(kbnClient, vmName); +}; From 931e7a62c595f83530612565ac94783d714a5dac Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Tue, 18 Apr 2023 08:13:12 -0400 Subject: [PATCH 02/24] Scripts: added `getAgentDownloadUrl()` to `common/fleet_services` (copy of version found in run endpoint host utility) --- .../scripts/endpoint/common/fleet_services.ts | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts index 1f4d28cecc569..e3ec3ad150340 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts @@ -27,6 +27,7 @@ import type { GetAgentsRequest, GetEnrollmentAPIKeysResponse, } from '@kbn/fleet-plugin/common/types'; +import nodeFetch from 'node-fetch'; import { FleetAgentGenerator } from '../../../common/endpoint/data_generators/fleet_agent_generator'; const fleetGenerator = new FleetAgentGenerator(); @@ -236,3 +237,54 @@ export const getAgentVersionMatchingCurrentStack = async ( return version; }; + +interface ElasticArtifactSearchResponse { + manifest: { + 'last-update-time': string; + 'seconds-since-last-update': number; + }; + packages: { + [packageFileName: string]: { + architecture: string; + os: string[]; + type: string; + asc_url: string; + sha_url: string; + url: string; + }; + }; +} + +/** + * Retrieves the download URL to the Linux installation package for a given version of the Elastic Agent + * @param version + * @param log + */ +export const getAgentDownloadUrl = async (version: string, log?: ToolingLog): Promise => { + const downloadArch = + { arm64: 'arm64', x64: 'x86_64' }[process.arch] ?? `UNSUPPORTED_ARCHITECTURE_${process.arch}`; + const agentFile = `elastic-agent-${version}-linux-${downloadArch}.tar.gz`; + const artifactSearchUrl = `https://artifacts-api.elastic.co/v1/search/${version}/${agentFile}`; + + log?.verbose(`Retrieving elastic agent download URL from:\n ${artifactSearchUrl}`); + + const searchResult: ElasticArtifactSearchResponse = await nodeFetch(artifactSearchUrl).then( + (response) => { + if (!response.ok) { + throw new Error( + `Failed to search elastic's artifact repository: ${response.statusText} (HTTP ${response.status})` + ); + } + + return response.json(); + } + ); + + log?.verbose(searchResult); + + if (!searchResult.packages[agentFile]) { + throw new Error(`Unable to find an Agent download URL for version [${version}]`); + } + + return searchResult.packages[agentFile].url; +}; From 55fc86ceb4d79246bc04b987c96658cccc79c70b Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Tue, 18 Apr 2023 08:13:57 -0400 Subject: [PATCH 03/24] Scripts: Initial version of reusable Endpoint Host Services service --- .../endpoint/common/endpoint_host_services.ts | 72 ++++++++++++++++--- 1 file changed, 63 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_host_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_host_services.ts index 6fb643317ef72..c6c4123808d25 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_host_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_host_services.ts @@ -5,10 +5,17 @@ * 2.0. */ +import { kibanaPackageJson } from '@kbn/repo-info'; import type { KbnClient } from '@kbn/test'; import type { ToolingLog } from '@kbn/tooling-log'; import execa from 'execa'; -import { waitForHostToEnroll } from './fleet_services'; +import assert from 'assert'; +import { + fetchAgentPolicyEnrollmentKey, + fetchFleetServerUrl, + getAgentDownloadUrl, + waitForHostToEnroll, +} from './fleet_services'; export interface CreateAndEnrollEndpointHostOptions extends Pick { @@ -23,8 +30,6 @@ export interface CreateAndEnrollEndpointHostOptions } interface CreateAndEnrollEndpointHostResponse { - /** The virtualization software used to create the new VM */ - provider: 'multipass'; hostname: string; agentId: string; } @@ -32,12 +37,57 @@ interface CreateAndEnrollEndpointHostResponse { /** * Creates a new virtual machine (host) and enrolls that with Fleet */ -export const createAndEnrollEndpointHost = async ( - options: CreateAndEnrollEndpointHostOptions -): Promise => { - // FIXME:PT implement +export const createAndEnrollEndpointHost = async ({ + kbnClient, + log, + agentPolicyId, + cpus, + disk, + memory, + hostname, + version = kibanaPackageJson.version, +}: CreateAndEnrollEndpointHostOptions): Promise => { + const [vm, agentDownloadUrl, fleetServerUrl, enrollmentToken] = await Promise.all([ + createMultipassVm({ + vmName: hostname ?? `test.host.${Math.random().toString().substring(2, 6)}`, + disk, + cpus, + memory, + }), + + getAgentDownloadUrl(version, log), + + fetchFleetServerUrl(kbnClient), + + fetchAgentPolicyEnrollmentKey(kbnClient, agentPolicyId), + ]); + + // Some validations before we proceed + assert(agentDownloadUrl, 'Missing agent download URL'); + assert(fleetServerUrl, 'Fleet server URL not set'); + assert(enrollmentToken, 'No enrollment token'); + + log.verbose(`Enrolling host [${vm.vmName}] + with fleet-server [${fleetServerUrl}] + using enrollment token [${enrollmentToken}]`); + + const { agentId } = await enrollHostWithFleet({ + kbnClient, + log, + fleetServerUrl, + agentDownloadUrl, + enrollmentToken, + vmName: vm.vmName, + }); + + return { + hostname: vm.vmName, + agentId, + }; }; +// FIXME:PT create function to remove endpoint host + interface CreateMultipassVmOptions { vmName: string; /** Number of CPUs */ @@ -86,7 +136,7 @@ const enrollHostWithFleet = async ({ fleetServerUrl, agentDownloadUrl, enrollmentToken, -}: EnrollHostWithFleetOptions): Promise => { +}: EnrollHostWithFleetOptions): Promise<{ agentId: string }> => { const agentDownloadedFile = agentDownloadUrl.substring(agentDownloadUrl.lastIndexOf('/') + 1); const vmDirName = agentDownloadedFile.replace(/\.tar\.gz$/, ''); @@ -129,5 +179,9 @@ const enrollHostWithFleet = async ({ await execa(`multipass`, agentInstallArguments); log.info(`Waiting for Agent to check-in with Fleet`); - await waitForHostToEnroll(kbnClient, vmName); + const agent = await waitForHostToEnroll(kbnClient, vmName, 120000); + + return { + agentId: agent.id, + }; }; From 03e1b3e83e73fee0d89616e1d401bd3f87e13c29 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Tue, 18 Apr 2023 08:14:42 -0400 Subject: [PATCH 04/24] Initial file setup for Endpoint alerts tests --- .../cypress/e2e/endpoint/endpoint_alerts.cy.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts index 478cbd7c2590e..d3d582a14ad76 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts @@ -6,5 +6,18 @@ */ describe('Endpoint generated alerts', () => { - // FIXME:PT implement + before(() => { + // 1. Create agent policy with endpoint policy with all protections enabled + // 2. create and enroll new host with above agent policy + }); + + after(() => { + // 1. delete VM created + // 2, Force-delete host from fleet (so we can delete policy) + // 3, Removed policy created + }); + + it('should create an alert', () => { + // FIXME:PT implement test + }); }); From 84be7edabca0e76378eb88a91f9466debca325c2 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Tue, 18 Apr 2023 11:20:15 -0400 Subject: [PATCH 05/24] Added `getLatestAgentDownloadVersion` to scripts fleet services and enhanced `getAgentDownloadUrl()` to use it --- .../endpoint/common/endpoint_host_services.ts | 8 +-- .../scripts/endpoint/common/fleet_services.ts | 71 +++++++++++++++++-- 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_host_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_host_services.ts index c6c4123808d25..4fdb7a7896e25 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_host_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_host_services.ts @@ -29,7 +29,7 @@ export interface CreateAndEnrollEndpointHostOptions hostname?: string; } -interface CreateAndEnrollEndpointHostResponse { +export interface CreateAndEnrollEndpointHostResponse { hostname: string; agentId: string; } @@ -49,13 +49,13 @@ export const createAndEnrollEndpointHost = async ({ }: CreateAndEnrollEndpointHostOptions): Promise => { const [vm, agentDownloadUrl, fleetServerUrl, enrollmentToken] = await Promise.all([ createMultipassVm({ - vmName: hostname ?? `test.host.${Math.random().toString().substring(2, 6)}`, + vmName: hostname ?? `test-host-${Math.random().toString().substring(2, 6)}`, disk, cpus, memory, }), - getAgentDownloadUrl(version, log), + getAgentDownloadUrl(version, true, log), fetchFleetServerUrl(kbnClient), @@ -65,7 +65,7 @@ export const createAndEnrollEndpointHost = async ({ // Some validations before we proceed assert(agentDownloadUrl, 'Missing agent download URL'); assert(fleetServerUrl, 'Fleet server URL not set'); - assert(enrollmentToken, 'No enrollment token'); + assert(enrollmentToken, `No enrollment token for agent policy id [${agentPolicyId}]`); log.verbose(`Enrolling host [${vm.vmName}] with fleet-server [${fleetServerUrl}] diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts index e3ec3ad150340..73e60667e79fd 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts @@ -28,6 +28,7 @@ import type { GetEnrollmentAPIKeysResponse, } from '@kbn/fleet-plugin/common/types'; import nodeFetch from 'node-fetch'; +import semver from 'semver'; import { FleetAgentGenerator } from '../../../common/endpoint/data_generators/fleet_agent_generator'; const fleetGenerator = new FleetAgentGenerator(); @@ -258,13 +259,23 @@ interface ElasticArtifactSearchResponse { /** * Retrieves the download URL to the Linux installation package for a given version of the Elastic Agent * @param version + * @param closestMatch * @param log */ -export const getAgentDownloadUrl = async (version: string, log?: ToolingLog): Promise => { +export const getAgentDownloadUrl = async ( + version: string, + /** + * When set to true a check will be done to determine the latest version of the agent that + * is less than or equal to the `version` provided + */ + closestMatch: boolean = false, + log?: ToolingLog +): Promise => { + const agentVersion = closestMatch ? await getLatestAgentDownloadVersion(version, log) : version; const downloadArch = { arm64: 'arm64', x64: 'x86_64' }[process.arch] ?? `UNSUPPORTED_ARCHITECTURE_${process.arch}`; - const agentFile = `elastic-agent-${version}-linux-${downloadArch}.tar.gz`; - const artifactSearchUrl = `https://artifacts-api.elastic.co/v1/search/${version}/${agentFile}`; + const agentFile = `elastic-agent-${agentVersion}-linux-${downloadArch}.tar.gz`; + const artifactSearchUrl = `https://artifacts-api.elastic.co/v1/search/${agentVersion}/${agentFile}`; log?.verbose(`Retrieving elastic agent download URL from:\n ${artifactSearchUrl}`); @@ -272,7 +283,7 @@ export const getAgentDownloadUrl = async (version: string, log?: ToolingLog): Pr (response) => { if (!response.ok) { throw new Error( - `Failed to search elastic's artifact repository: ${response.statusText} (HTTP ${response.status})` + `Failed to search elastic's artifact repository: ${response.statusText} (HTTP ${response.status}) {URL: ${artifactSearchUrl})` ); } @@ -283,8 +294,58 @@ export const getAgentDownloadUrl = async (version: string, log?: ToolingLog): Pr log?.verbose(searchResult); if (!searchResult.packages[agentFile]) { - throw new Error(`Unable to find an Agent download URL for version [${version}]`); + throw new Error(`Unable to find an Agent download URL for version [${agentVersion}]`); } return searchResult.packages[agentFile].url; }; + +/** + * Given a stack version number, function will return the closest Agent download version available + * for download. THis could be the actual version passed in or lower. + * @param version + */ +export const getLatestAgentDownloadVersion = async ( + version: string, + log?: ToolingLog +): Promise => { + const artifactsUrl = 'https://artifacts-api.elastic.co/v1/versions'; + const semverMatch = `<=${version}`; + const artifactVersionsResponse: { versions: string[] } = await nodeFetch(artifactsUrl).then( + (response) => { + if (!response.ok) { + throw new Error( + `Failed to retrieve list of versions from elastic's artifact repository: ${response.statusText} (HTTP ${response.status}) {URL: ${artifactsUrl})` + ); + } + + return response.json(); + } + ); + + const stackVersionToArtifactVersion: Record = + artifactVersionsResponse.versions.reduce((acc, artifactVersion) => { + const stackVersion = artifactVersion.split('-SNAPSHOT')[0]; + acc[stackVersion] = artifactVersion; + return acc; + }, {} as Record); + + log?.verbose( + `Versions found from [${artifactsUrl}]:\n${JSON.stringify( + stackVersionToArtifactVersion, + null, + 2 + )}` + ); + + const matchedVersion = semver.maxSatisfying( + Object.keys(stackVersionToArtifactVersion), + semverMatch + ); + + if (!matchedVersion) { + throw new Error(`Unable to find a semver version that meets ${semverMatch}`); + } + + return stackVersionToArtifactVersion[matchedVersion]; +}; From 740f10bef5b50832fedfcad588b84fcfb0c62b94 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Tue, 18 Apr 2023 11:46:02 -0400 Subject: [PATCH 06/24] New `waitForEndpointToStreamData()` to script/common Endpoint Metadata Services --- .../common/endpoint_metadata_services.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_metadata_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_metadata_services.ts index a1f21b80567d6..115dd025ac398 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_metadata_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_metadata_services.ts @@ -131,3 +131,41 @@ const fetchLastStreamedEndpointUpdate = async ( return queryResult.hits?.hits[0]?._source; }; + +/** + * Waits for an endpoint to have streamed data to ES and for that data to have made it to the + * Endpoint Details API (transform destination index) + * @param kbnClient + * @param endpointAgentId + * @param timeoutMs + */ +export const waitForEndpointToStreamData = async ( + kbnClient: KbnClient, + endpointAgentId: string, + timeoutMs: number = 60000 +): Promise => { + const started = new Date(); + const hasTimedOut = (): boolean => { + const elapsedTime = Date.now() - started.getTime(); + return elapsedTime > timeoutMs; + }; + let found: HostInfo | undefined; + + while (!found && !hasTimedOut()) { + found = await fetchEndpointMetadata(kbnClient, endpointAgentId).catch((error) => { + // FIXME:PT check errors, ignore 404 + return undefined; + }); + + if (!found) { + // sleep and check again + await new Promise((r) => setTimeout(r, 2000)); + } + } + + if (!found) { + throw new Error(`Timed out waiting for Endpoint id [${endpointAgentId}] to stream data to ES`); + } + + return found; +}; From 5fe4fbb48f71b74876feb291d7880ca675882234 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Tue, 18 Apr 2023 11:47:20 -0400 Subject: [PATCH 07/24] Cy endpoint policy task to enable all protections --- .../cypress/tasks/endpoint_policy.ts | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 x-pack/plugins/security_solution/public/management/cypress/tasks/endpoint_policy.ts diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/endpoint_policy.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/endpoint_policy.ts new file mode 100644 index 0000000000000..134fc470b412b --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/endpoint_policy.ts @@ -0,0 +1,63 @@ +/* + * 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 { + GetOnePackagePolicyResponse, + UpdatePackagePolicy, + UpdatePackagePolicyResponse, +} from '@kbn/fleet-plugin/common'; +import { packagePolicyRouteService } from '@kbn/fleet-plugin/common'; +import { request } from './common'; +import { ProtectionModes } from '../../../../common/endpoint/types'; + +/** + * Updates the given Endpoint policy and enables all of the policy protections + * @param endpointPolicyId + */ +export const enableAllPolicyProtections = ( + endpointPolicyId: string +): Cypress.Chainable> => { + return request({ + method: 'GET', + url: packagePolicyRouteService.getInfoPath(endpointPolicyId), + }).then(({ body: { item: endpointPolicy } }) => { + const { + created_by: _createdBy, + created_at: _createdAt, + updated_at: _updatedAt, + updated_by: _updatedBy, + id, + version, + revision, + ...restOfPolicy + } = endpointPolicy; + + const updatedEndpointPolicy: UpdatePackagePolicy = restOfPolicy; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const policy = updatedEndpointPolicy!.inputs[0]!.config!.policy.value; + + policy.mac.malware.mode = ProtectionModes.prevent; + policy.windows.malware.mode = ProtectionModes.prevent; + policy.linux.malware.mode = ProtectionModes.prevent; + + policy.mac.memory_protection.mode = ProtectionModes.prevent; + policy.windows.memory_protection.mode = ProtectionModes.prevent; + policy.linux.memory_protection.mode = ProtectionModes.prevent; + + policy.mac.behavior_protection.mode = ProtectionModes.prevent; + policy.windows.behavior_protection.mode = ProtectionModes.prevent; + policy.linux.behavior_protection.mode = ProtectionModes.prevent; + + policy.windows.ransomware.mode = ProtectionModes.prevent; + + return request({ + method: 'PUT', + url: packagePolicyRouteService.getUpdatePath(endpointPolicyId), + body: updatedEndpointPolicy, + }); + }); +}; From 86b99cd2fd3ed3666e820096e1d47398cfb714f5 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Tue, 18 Apr 2023 11:48:39 -0400 Subject: [PATCH 08/24] Added `dataLoadersForRealEndpoints()` support for endpoint suite --- .../management/cypress/screens/endpoints.ts | 6 +++- .../cypress/support/data_loaders.ts | 36 +++++++++++++++++++ .../management/cypress_endpoint.config.ts | 6 ++-- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/cypress/screens/endpoints.ts b/x-pack/plugins/security_solution/public/management/cypress/screens/endpoints.ts index 32a12168aadb0..da2c278b8e30d 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/screens/endpoints.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/screens/endpoints.ts @@ -6,7 +6,7 @@ */ import { APP_PATH } from '../../../../common/constants'; -import { getEndpointDetailsPath } from '../../common/routing'; +import { getEndpointDetailsPath, getEndpointListPath } from '../../common/routing'; export const AGENT_HOSTNAME_CELL = 'hostnameCellLink'; export const AGENT_POLICY_CELL = 'policyNameCellLink'; @@ -21,3 +21,7 @@ export const navigateToEndpointPolicyResponse = ( getEndpointDetailsPath({ name: 'endpointPolicyResponse', selected_endpoint: endpointAgentId }) ); }; + +export const navigateToEndpointList = (): Cypress.Chainable => { + return cy.visit(APP_PATH + getEndpointListPath({ name: 'endpointList' })); +}; 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..75ce4010a6a0b 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,11 @@ // / import type { CasePostRequest } from '@kbn/cases-plugin/common/api'; +import { waitForEndpointToStreamData } from '../../../../scripts/endpoint/common/endpoint_metadata_services'; +import type { + CreateAndEnrollEndpointHostOptions, + CreateAndEnrollEndpointHostResponse, +} from '../../../../scripts/endpoint/common/endpoint_host_services'; import type { IndexedEndpointPolicyResponse } from '../../../../common/endpoint/data_loaders/index_endpoint_policy_response'; import { deleteIndexedEndpointPolicyResponse, @@ -34,6 +39,7 @@ import { deleteIndexedEndpointRuleAlerts, indexEndpointRuleAlerts, } from '../../../../common/endpoint/data_loaders/index_endpoint_rule_alerts'; +import { createAndEnrollEndpointHost } from '../../../../scripts/endpoint/common/endpoint_host_services'; /** * Cypress plugin for adding data loading related `task`s @@ -142,3 +148,33 @@ export const dataLoaders = ( }, }); }; + +export const dataLoadersForRealEndpoints = ( + on: Cypress.PluginEvents, + config: Cypress.PluginConfigOptions +): void => { + const stackServicesPromise = createRuntimeServices({ + kibanaUrl: config.env.KIBANA_URL, + elasticsearchUrl: config.env.ELASTICSEARCH_URL, + username: config.env.ELASTICSEARCH_USERNAME, + password: config.env.ELASTICSEARCH_PASSWORD, + asSuperuser: true, + }); + + on('task', { + createEndpointHost: async ( + options: Omit + ): Promise => { + const { kbnClient, log } = await stackServicesPromise; + return createAndEnrollEndpointHost({ ...options, log, kbnClient }).then((newHost) => { + return waitForEndpointToStreamData(kbnClient, newHost.agentId, 120000).then(() => { + return newHost; + }); + }); + }, + + destroyEndpointHost: async () => { + // FIXME:PT implement + }, + }); +}; diff --git a/x-pack/plugins/security_solution/public/management/cypress_endpoint.config.ts b/x-pack/plugins/security_solution/public/management/cypress_endpoint.config.ts index 8975599350fe2..50a9d8f1f5356 100644 --- a/x-pack/plugins/security_solution/public/management/cypress_endpoint.config.ts +++ b/x-pack/plugins/security_solution/public/management/cypress_endpoint.config.ts @@ -7,7 +7,7 @@ import { defineCypressConfig } from '@kbn/cypress-config'; // eslint-disable-next-line @kbn/imports/no_boundary_crossing -import { dataLoaders } from './cypress/support/data_loaders'; +import { dataLoaders, dataLoadersForRealEndpoints } from './cypress/support/data_loaders'; // eslint-disable-next-line import/no-default-export export default defineCypressConfig({ @@ -40,7 +40,9 @@ export default defineCypressConfig({ specPattern: 'public/management/cypress/e2e/endpoint/*.cy.{js,jsx,ts,tsx}', experimentalRunAllSpecs: true, setupNodeEvents: (on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions) => { - return dataLoaders(on, config); + dataLoaders(on, config); + // Data loaders specific to "real" Endpoint testing + dataLoadersForRealEndpoints(on, config); }, }, }); From c0a43a1e802daf60343b3a189c80661dabfbe46a Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Tue, 18 Apr 2023 11:56:30 -0400 Subject: [PATCH 09/24] Cy data load of real endpoint done in test --- .../e2e/endpoint/endpoint_alerts.cy.ts | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts index d3d582a14ad76..029b27644a5d2 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts @@ -5,19 +5,61 @@ * 2.0. */ +import { navigateToEndpointList } from '../../screens/endpoints'; +import { getEndpointIntegrationVersion } from '../../tasks/fleet'; +import type { IndexedFleetEndpointPolicyResponse } from '../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy'; +import { enableAllPolicyProtections } from '../../tasks/endpoint_policy'; +import type { PolicyData } from '../../../../../common/endpoint/types'; +import type { CreateAndEnrollEndpointHostResponse } from '../../../../../scripts/endpoint/common/endpoint_host_services'; +import { login } from '../../tasks/login'; + describe('Endpoint generated alerts', () => { + let indexedPolicy: IndexedFleetEndpointPolicyResponse; + let policy: PolicyData; + let createdHost: CreateAndEnrollEndpointHostResponse; + before(() => { - // 1. Create agent policy with endpoint policy with all protections enabled - // 2. create and enroll new host with above agent policy + getEndpointIntegrationVersion().then((version) => { + const policyName = `alerts test ${Math.random().toString(36).substring(2, 7)}`; + + cy.task('indexFleetEndpointPolicy', { + policyName, + endpointPackageVersion: version, + agentPolicyName: policyName, + }).then((data) => { + indexedPolicy = data; + policy = indexedPolicy.integrationPolicies[0]; + + return enableAllPolicyProtections(policy.id).then(() => { + // Create and enroll a new Endpoint host + return cy + .task( + 'createEndpointHost', + { + agentPolicyId: policy.policy_id, + }, + { timeout: 120000 } + ) + .then((host) => { + createdHost = host as CreateAndEnrollEndpointHostResponse; + }); + }); + }); + }); }); after(() => { + // FIXME:PT implement // 1. delete VM created // 2, Force-delete host from fleet (so we can delete policy) // 3, Removed policy created }); + beforeEach(() => { + login(); + }); + it('should create an alert', () => { - // FIXME:PT implement test + navigateToEndpointList(); }); }); From 1eaa2f9d2da784637ab368853ea4970f796f3809 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Wed, 19 Apr 2023 08:22:16 -0400 Subject: [PATCH 10/24] enable experimental feature in Endpoint Cy config --- x-pack/test/defend_workflows_cypress/endpoint_config.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x-pack/test/defend_workflows_cypress/endpoint_config.ts b/x-pack/test/defend_workflows_cypress/endpoint_config.ts index f1ea9a9c81a12..e6191cea7074b 100644 --- a/x-pack/test/defend_workflows_cypress/endpoint_config.ts +++ b/x-pack/test/defend_workflows_cypress/endpoint_config.ts @@ -8,6 +8,7 @@ import { getLocalhostRealIp } from '@kbn/security-solution-plugin/scripts/endpoint/common/localhost_services'; import { FtrConfigProviderContext } from '@kbn/test'; +import { ExperimentalFeatures } from '@kbn/security-solution-plugin/common/experimental_features'; import { DefendWorkflowsCypressEndpointTestRunner } from './runner'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { @@ -15,6 +16,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { const config = defendWorkflowsCypressConfig.getAll(); const hostIp = getLocalhostRealIp(); + const enabledFeatureFlags: Array = ['responseActionExecuteEnabled']; + return { ...config, kbnTestServer: { @@ -27,6 +30,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { )}`, // set the packagerTaskInterval to 5s in order to speed up test executions when checking fleet artifacts '--xpack.securitySolution.packagerTaskInterval=5s', + `--xpack.securitySolution.enableExperimental=${JSON.stringify(enabledFeatureFlags)}`, ], }, testRunner: DefendWorkflowsCypressEndpointTestRunner, From 7f61e7d7253d507359db0b3fc93b58a383342095 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Wed, 19 Apr 2023 10:31:38 -0400 Subject: [PATCH 11/24] added `waitForActionToComplete` to response actions tasks --- .../cypress/tasks/response_actions.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts index c888e7dce1254..0da2c51a20d3d 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts @@ -5,6 +5,10 @@ * 2.0. */ +import { request } from './common'; +import { resolvePathVariables } from '../../../common/utils/resolve_path_variables'; +import { ACTION_DETAILS_ROUTE } from '../../../../common/endpoint/constants'; +import type { ActionDetailsApiResponse } from '../../../../common/endpoint/types'; import { ENABLED_AUTOMATED_RESPONSE_ACTION_COMMANDS } from '../../../../common/endpoint/service/response_actions/constants'; export const validateAvailableCommands = () => { @@ -59,3 +63,35 @@ export const tryAddingDisabledResponseAction = (itemNumber = 0) => { }); cy.getByTestSubj(`response-actions-list-item-${itemNumber}`).should('not.exist'); }; + +/** + * Continuously checks an Response Action until it completes (or timeout is reached) + * @param actionId + * @param timeout + */ +export const waitForActionToComplete = (actionId: string, timeout = 60000) => { + let timeLeft = timeout; + + const checkAction = () => { + request({ + method: 'GET', + url: resolvePathVariables(ACTION_DETAILS_ROUTE, { action_id: actionId || 'undefined' }), + }).then((response) => { + if (response.body.data.isCompleted) { + return; + } + + timeLeft -= 1000; + + if (timeLeft <= 0) { + throw new Error(`Timed out waiting for action [${actionId}] to complete`); + } + + cy.wait(1000); + + checkAction(); + }); + }; + + checkAction(); +}; From 2ec7f548b43e9164304384409d5e34078a3a25f8 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Wed, 19 Apr 2023 11:05:35 -0400 Subject: [PATCH 12/24] Added new Cy `waitUntil()` and refactored `waitForActionToComplete()` to use it --- .../public/management/cypress/cypress.d.ts | 14 ++++++ .../public/management/cypress/support/e2e.ts | 38 +++++++++++++++ .../cypress/tasks/response_actions.ts | 47 ++++++++++--------- 3 files changed, 78 insertions(+), 21 deletions(-) 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..a32de299bf86c 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 @@ -53,6 +53,20 @@ declare global { ...args: Parameters['find']> ): Chainable>; + /** + * Continuously call provided callback function until it either return `true` + * or fail if `timeout` is reached. + * @param fn + * @param options + */ + waitUntil( + fn: (subject?: any) => boolean | Promise | Chainable, + options?: Partial<{ + interval: number; + timeout: number; + }> + ): Chainable; + task( name: 'indexFleetEndpointPolicy', arg: { diff --git a/x-pack/plugins/security_solution/public/management/cypress/support/e2e.ts b/x-pack/plugins/security_solution/public/management/cypress/support/e2e.ts index 0ccb00e8d5e63..12c236f481791 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/support/e2e.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/support/e2e.ts @@ -50,4 +50,42 @@ Cypress.Commands.addQuery<'findByTestSubj'>( } ); +Cypress.Commands.add( + 'waitUntil', + { prevSubject: 'optional' }, + (subject, fn, { interval = 500, timeout = 30000 } = {}) => { + let attempts = Math.floor(timeout / interval); + + const completeOrRetry = (result: boolean) => { + if (result) { + return result; + } + if (attempts < 1) { + throw new Error(`Timed out while retrying, last result was: {${result}}`); + } + cy.wait(interval, { log: false }).then(() => { + attempts--; + return evaluate(); + }); + }; + + const evaluate = () => { + const result = fn(subject); + + if (typeof result === 'boolean') { + return completeOrRetry(result); + } else if ('then' in result) { + // @ts-expect-error + return result.then(completeOrRetry); + } else { + throw new Error( + `Unknown return type from callback: ${Object.prototype.toString.call(result)}` + ); + } + }; + + return evaluate(); + } +); + Cypress.on('uncaught:exception', () => false); diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts index 0da2c51a20d3d..62ea4feed7efc 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts @@ -8,7 +8,7 @@ import { request } from './common'; import { resolvePathVariables } from '../../../common/utils/resolve_path_variables'; import { ACTION_DETAILS_ROUTE } from '../../../../common/endpoint/constants'; -import type { ActionDetailsApiResponse } from '../../../../common/endpoint/types'; +import type { ActionDetails, ActionDetailsApiResponse } from '../../../../common/endpoint/types'; import { ENABLED_AUTOMATED_RESPONSE_ACTION_COMMANDS } from '../../../../common/endpoint/service/response_actions/constants'; export const validateAvailableCommands = () => { @@ -69,29 +69,34 @@ export const tryAddingDisabledResponseAction = (itemNumber = 0) => { * @param actionId * @param timeout */ -export const waitForActionToComplete = (actionId: string, timeout = 60000) => { - let timeLeft = timeout; +export const waitForActionToComplete = ( + actionId: string, + timeout = 60000 +): Cypress.Chainable => { + let action: ActionDetails | undefined; - const checkAction = () => { - request({ - method: 'GET', - url: resolvePathVariables(ACTION_DETAILS_ROUTE, { action_id: actionId || 'undefined' }), - }).then((response) => { - if (response.body.data.isCompleted) { - return; - } - - timeLeft -= 1000; + return cy + .waitUntil( + () => { + return request({ + method: 'GET', + url: resolvePathVariables(ACTION_DETAILS_ROUTE, { action_id: actionId || 'undefined' }), + }).then((response) => { + if (response.body.data.isCompleted) { + action = response.body.data; + return true; + } - if (timeLeft <= 0) { - throw new Error(`Timed out waiting for action [${actionId}] to complete`); + return false; + }); + }, + { timeout } + ) + .then(() => { + if (!action) { + throw new Error(`Unable to completed action`); } - cy.wait(1000); - - checkAction(); + return action; }); - }; - - checkAction(); }; From 06e24189ca06b598db6dc8bc866179fb9348fd81 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Wed, 19 Apr 2023 11:25:27 -0400 Subject: [PATCH 13/24] initial structure for checking that alerts are received from endpoint --- .../e2e/endpoint/endpoint_alerts.cy.ts | 33 +++++++++++++++++-- .../public/management/cypress/tasks/alerts.ts | 19 +++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/cypress/tasks/alerts.ts diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts index 029b27644a5d2..83fa640ad871d 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts @@ -5,13 +5,17 @@ * 2.0. */ +import { waitForEndpointAlerts } from '../../tasks/alerts'; +import { request } from '../../tasks/common'; import { navigateToEndpointList } from '../../screens/endpoints'; import { getEndpointIntegrationVersion } from '../../tasks/fleet'; import type { IndexedFleetEndpointPolicyResponse } from '../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy'; import { enableAllPolicyProtections } from '../../tasks/endpoint_policy'; -import type { PolicyData } from '../../../../../common/endpoint/types'; +import type { PolicyData, ResponseActionApiResponse } from '../../../../../common/endpoint/types'; import type { CreateAndEnrollEndpointHostResponse } from '../../../../../scripts/endpoint/common/endpoint_host_services'; import { login } from '../../tasks/login'; +import { EXECUTE_ROUTE } from '../../../../../common/endpoint/constants'; +import { waitForActionToComplete } from '../../tasks/response_actions'; describe('Endpoint generated alerts', () => { let indexedPolicy: IndexedFleetEndpointPolicyResponse; @@ -53,13 +57,38 @@ describe('Endpoint generated alerts', () => { // 1. delete VM created // 2, Force-delete host from fleet (so we can delete policy) // 3, Removed policy created + // + // + // ?. Clean up: + // created action + // created action response + // alerts/events + // files created by potential endpoint actions }); beforeEach(() => { login(); }); - it('should create an alert', () => { + it('should create a Detection Engine alert from an endpoint alert', () => { + // FIXME:PT remove this (only for dev) navigateToEndpointList(); + + // 1. send `execute` command that triggers malicious behaviour + request({ + method: 'POST', + url: EXECUTE_ROUTE, + body: { + endpoint_ids: [createdHost.agentId], + parameters: { + // Triggers a Malicious Behaviour alert on Linux system + command: 'bash -c cat /dev/tcp/foo', + }, + }, + }) + .then((response) => waitForActionToComplete(response.body.data.id)) + .then(() => waitForEndpointAlerts(createdHost.agentId)); + + // 4. check that alert show up on alerts list }); }); diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/alerts.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/alerts.ts new file mode 100644 index 0000000000000..91e217209bf8e --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/alerts.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +/** + * Continuously check for any alert to have been received by the given endpoint + */ +export const waitForEndpointAlerts = (endpointAgentId: string) => { + // 0. get a baseline - query the index that teh endpoint streams data to to get a current total + // + // 1. Check index that endpoint streams alerts to for new ones to show up + // + // 2. once received, stop/start Endpoint rule + // + // 3. wait until the Detection alert show up +}; From d41e63c8bf95b813dccae52caae989ea5c3641ba Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Wed, 19 Apr 2023 17:49:23 -0400 Subject: [PATCH 14/24] waits for alert to be streamed by endpoint --- .../e2e/endpoint/endpoint_alerts.cy.ts | 16 ++++-- .../public/management/cypress/tasks/alerts.ts | 53 ++++++++++++++++--- .../scripts/endpoint/common/stack_services.ts | 6 ++- 3 files changed, 63 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts index 83fa640ad871d..ec42ebab05454 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts @@ -42,7 +42,7 @@ describe('Endpoint generated alerts', () => { { agentPolicyId: policy.policy_id, }, - { timeout: 120000 } + { timeout: 180000 } ) .then((host) => { createdHost = host as CreateAndEnrollEndpointHostResponse; @@ -74,6 +74,8 @@ describe('Endpoint generated alerts', () => { // FIXME:PT remove this (only for dev) navigateToEndpointList(); + const executeCommand = `bash -c cat /dev/tcp/foo | grep ${createdHost.agentId}`; + // 1. send `execute` command that triggers malicious behaviour request({ method: 'POST', @@ -82,13 +84,21 @@ describe('Endpoint generated alerts', () => { endpoint_ids: [createdHost.agentId], parameters: { // Triggers a Malicious Behaviour alert on Linux system - command: 'bash -c cat /dev/tcp/foo', + command: executeCommand, }, }, }) .then((response) => waitForActionToComplete(response.body.data.id)) - .then(() => waitForEndpointAlerts(createdHost.agentId)); + .then(() => + waitForEndpointAlerts(createdHost.agentId, [ + { + term: { 'process.group_leader.args': executeCommand }, + }, + ]) + ); // 4. check that alert show up on alerts list + + cy.log('Reached end of test'); }); }); diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/alerts.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/alerts.ts index 91e217209bf8e..d0c94c2eda4dc 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/alerts.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/alerts.ts @@ -5,15 +5,52 @@ * 2.0. */ +import type { estypes } from '@elastic/elasticsearch'; +import { ENDPOINT_ALERTS_INDEX } from '../../../../scripts/endpoint/common/constants'; +import { request } from './common'; +const ES_URL = Cypress.env('ELASTICSEARCH_URL'); + /** * Continuously check for any alert to have been received by the given endpoint */ -export const waitForEndpointAlerts = (endpointAgentId: string) => { - // 0. get a baseline - query the index that teh endpoint streams data to to get a current total - // - // 1. Check index that endpoint streams alerts to for new ones to show up - // - // 2. once received, stop/start Endpoint rule - // - // 3. wait until the Detection alert show up +export const waitForEndpointAlerts = ( + endpointAgentId: string, + additionalFilters?: object[], + timeout = 120000 +) => { + return ( + cy + .waitUntil( + () => { + return request({ + method: 'GET', + url: `${ES_URL}/${ENDPOINT_ALERTS_INDEX}/_search`, + body: { + query: { + match: { + 'agent.id': endpointAgentId, + }, + }, + size: 1, + _source: false, + }, + }).then(({ body: streamedAlerts }) => { + return (streamedAlerts.hits.total as estypes.SearchTotalHits).value > 0; + }); + }, + { timeout } + ) + .then(() => { + // Stop/start Endpoint rule so that it can pickup and create Detection alerts + cy.log( + `Received endpoint alerts for agent [${endpointAgentId}] in index [${ENDPOINT_ALERTS_INDEX}]` + ); + + // + }) + // 3. wait until the Detection alert shows up in the API + .then(() => { + // + }) + ); }; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts index 424f451c3fdc6..f7ba4c1a5b514 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts @@ -99,7 +99,11 @@ export const createRuntimeServices = async ({ }; }; -const buildUrlWithCredentials = (url: string, username: string, password: string): string => { +export const buildUrlWithCredentials = ( + url: string, + username: string, + password: string +): string => { const newUrl = new URL(url); newUrl.username = username; From 7b318791eb29a7bba4b3ddfb8a0fcf8508b425c4 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Thu, 20 Apr 2023 10:09:52 -0400 Subject: [PATCH 15/24] cy: additional alerts related tasks --- .../public/management/cypress/tasks/alerts.ts | 182 ++++++++++++++---- 1 file changed, 148 insertions(+), 34 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/alerts.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/alerts.ts index d0c94c2eda4dc..a78b1c6742afa 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/alerts.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/alerts.ts @@ -6,51 +6,165 @@ */ import type { estypes } from '@elastic/elasticsearch'; -import { ENDPOINT_ALERTS_INDEX } from '../../../../scripts/endpoint/common/constants'; +import type { Rule } from '../../../detection_engine/rule_management/logic'; +import { + DETECTION_ENGINE_QUERY_SIGNALS_URL, + DETECTION_ENGINE_RULES_BULK_ACTION, + DETECTION_ENGINE_RULES_URL, +} from '../../../../common/constants'; +import { ELASTIC_SECURITY_RULE_ID } from '../../../../common'; import { request } from './common'; +import { ENDPOINT_ALERTS_INDEX } from '../../../../scripts/endpoint/common/constants'; const ES_URL = Cypress.env('ELASTICSEARCH_URL'); /** - * Continuously check for any alert to have been received by the given endpoint + * Continuously check for any alert to have been received by the given endpoint. + * + * NOTE: This is tno the same as the alerts that populate the Alerts list. To check for + * those types of alerts, use `waitForDetectionAlerts()` */ export const waitForEndpointAlerts = ( endpointAgentId: string, additionalFilters?: object[], timeout = 120000 -) => { - return ( - cy - .waitUntil( - () => { - return request({ - method: 'GET', - url: `${ES_URL}/${ENDPOINT_ALERTS_INDEX}/_search`, - body: { - query: { - match: { - 'agent.id': endpointAgentId, - }, +): Cypress.Chainable => { + return cy + .waitUntil( + () => { + return request({ + method: 'GET', + url: `${ES_URL}/${ENDPOINT_ALERTS_INDEX}/_search`, + body: { + query: { + match: { + 'agent.id': endpointAgentId, }, - size: 1, - _source: false, }, - }).then(({ body: streamedAlerts }) => { - return (streamedAlerts.hits.total as estypes.SearchTotalHits).value > 0; - }); + size: 1, + _source: false, + }, + }).then(({ body: streamedAlerts }) => { + return (streamedAlerts.hits.total as estypes.SearchTotalHits).value > 0; + }); + }, + { timeout } + ) + .then(() => { + // Stop/start Endpoint rule so that it can pickup and create Detection alerts + cy.log( + `Received endpoint alerts for agent [${endpointAgentId}] in index [${ENDPOINT_ALERTS_INDEX}]` + ); + + return stopStartEndpointDetectionsRule(); + }) + .then(() => { + // wait until the Detection alert shows up in the API + return waitForDetectionAlerts(getEndpointDetectionAlertsQueryForAgentId(endpointAgentId)); + }); +}; + +export const fetchEndpointSecurityDetectionRule = (): Cypress.Chainable => { + return request({ + method: 'GET', + url: DETECTION_ENGINE_RULES_URL, + qs: { + rule_id: ELASTIC_SECURITY_RULE_ID, + }, + }).then(({ body }) => { + return body; + }); +}; + +export const stopStartEndpointDetectionsRule = (): Cypress.Chainable => { + return fetchEndpointSecurityDetectionRule() + .then((endpointRule) => { + // Disabled it + return request({ + method: 'POST', + url: DETECTION_ENGINE_RULES_BULK_ACTION, + body: { + action: 'disable', + ids: [endpointRule.id], + }, + }).then(() => { + return endpointRule; + }); + }) + .then((endpointRule) => { + cy.log(`Endpoint rule id [${endpointRule.id}] has been disabled`); + + // Re-enable it + return request({ + method: 'POST', + url: DETECTION_ENGINE_RULES_BULK_ACTION, + body: { + action: 'enable', + ids: [endpointRule.id], }, - { timeout } - ) - .then(() => { - // Stop/start Endpoint rule so that it can pickup and create Detection alerts - cy.log( - `Received endpoint alerts for agent [${endpointAgentId}] in index [${ENDPOINT_ALERTS_INDEX}]` - ); - - // - }) - // 3. wait until the Detection alert shows up in the API - .then(() => { - // - }) + }).then(() => endpointRule); + }) + .then((endpointRule) => { + cy.log(`Endpoint rule id [${endpointRule.id}] has been re-enabled`); + return cy.wrap(endpointRule); + }); +}; + +/** + * Waits for alerts to have been loaded by continuously calling the detections engine alerts + * api until data shows up + * @param query + * @param timeout + */ +export const waitForDetectionAlerts = ( + /** The ES query. Defaults to `{ match_all: {} }` */ + query: object = { match_all: {} }, + timeout?: number +): Cypress.Chainable => { + return cy.waitUntil( + () => { + return request({ + method: 'POST', + url: DETECTION_ENGINE_QUERY_SIGNALS_URL, + body: { + query, + size: 1, + }, + }).then(({ body: alertsResponse }) => { + return Boolean((alertsResponse.hits.total as estypes.SearchTotalHits)?.value ?? 0); + }); + }, + { timeout } ); }; + +/** + * Builds and returns the ES `query` object for use in querying for Endpoint Detection Engine + * alerts. Can be used in ES searches or with the Detection Engine query signals (alerts) url. + * @param endpointAgentId + */ +export const getEndpointDetectionAlertsQueryForAgentId = (endpointAgentId: string) => { + return { + bool: { + filter: [ + { + bool: { + should: [{ match_phrase: { 'agent.type': 'endpoint' } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [{ match_phrase: { 'agent.id': endpointAgentId } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [{ exists: { field: 'kibana.alert.rule.uuid' } }], + minimum_should_match: 1, + }, + }, + ], + }, + }; +}; From ccb69263cd65aed675c92ba0d1d8ebba474956dd Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Thu, 20 Apr 2023 10:10:25 -0400 Subject: [PATCH 16/24] cy: alerts related screen selectors --- .../management/cypress/screens/alerts.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 x-pack/plugins/security_solution/public/management/cypress/screens/alerts.ts diff --git a/x-pack/plugins/security_solution/public/management/cypress/screens/alerts.ts b/x-pack/plugins/security_solution/public/management/cypress/screens/alerts.ts new file mode 100644 index 0000000000000..48f0747464bf8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/screens/alerts.ts @@ -0,0 +1,41 @@ +/* + * 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 { APP_ALERTS_PATH } from '../../../../common/constants'; + +export const navigateToAlertsList = (urlQueryParams: string = '') => { + cy.visit(`${APP_ALERTS_PATH}${urlQueryParams ? `?${urlQueryParams}` : ''}`); +}; + +export const clickAlertListRefreshButton = (): Cypress.Chainable => { + return cy.getByTestSubj('querySubmitButton').click().should('be.enabled'); +}; + +/** + * Waits until the Alerts list has alerts data and return the number of rows that are currently displayed + * @param timeout + */ +export const getAlertsTableRows = (timeout?: number): Cypress.Chainable> => { + let $rows: JQuery = Cypress.$(); + + return cy + .waitUntil( + () => { + clickAlertListRefreshButton(); + + return cy + .getByTestSubj('alertsTable') + .find('.euiDataGridRow') + .then(($rowsFound) => { + $rows = $rowsFound; + return Boolean($rows); + }); + }, + { timeout } + ) + .then(() => $rows); +}; From edd96e4892451b14b53eec88706e42384c00dee4 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Thu, 20 Apr 2023 10:10:56 -0400 Subject: [PATCH 17/24] cy: complete working test for alerts --- .../e2e/endpoint/endpoint_alerts.cy.ts | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts index ec42ebab05454..a218679a6ae73 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts @@ -5,9 +5,9 @@ * 2.0. */ +import { getAlertsTableRows, navigateToAlertsList } from '../../screens/alerts'; import { waitForEndpointAlerts } from '../../tasks/alerts'; import { request } from '../../tasks/common'; -import { navigateToEndpointList } from '../../screens/endpoints'; import { getEndpointIntegrationVersion } from '../../tasks/fleet'; import type { IndexedFleetEndpointPolicyResponse } from '../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy'; import { enableAllPolicyProtections } from '../../tasks/endpoint_policy'; @@ -71,34 +71,34 @@ describe('Endpoint generated alerts', () => { }); it('should create a Detection Engine alert from an endpoint alert', () => { - // FIXME:PT remove this (only for dev) - navigateToEndpointList(); + // Triggers a Malicious Behaviour alert on Linux system + const executeMaliciousCommand = `bash -c cat /dev/tcp/foo | grep ${createdHost.agentId}`; - const executeCommand = `bash -c cat /dev/tcp/foo | grep ${createdHost.agentId}`; - - // 1. send `execute` command that triggers malicious behaviour + // Send `execute` command that triggers malicious behaviour using the `execute` response action request({ method: 'POST', url: EXECUTE_ROUTE, body: { endpoint_ids: [createdHost.agentId], parameters: { - // Triggers a Malicious Behaviour alert on Linux system - command: executeCommand, + command: executeMaliciousCommand, }, }, }) .then((response) => waitForActionToComplete(response.body.data.id)) - .then(() => - waitForEndpointAlerts(createdHost.agentId, [ + .then(() => { + return waitForEndpointAlerts(createdHost.agentId, [ { - term: { 'process.group_leader.args': executeCommand }, + term: { 'process.group_leader.args': executeMaliciousCommand }, }, - ]) - ); - - // 4. check that alert show up on alerts list + ]); + }) + .then(() => { + return navigateToAlertsList( + `query=(language:kuery,query:'agent.id: "${createdHost.agentId}" ')` + ); + }); - cy.log('Reached end of test'); + getAlertsTableRows().should('have.length.greaterThan', 0); }); }); From 731666bc913715ea14a2b16a8df7796fa76de86a Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Thu, 20 Apr 2023 10:48:31 -0400 Subject: [PATCH 18/24] Destroy VM + unenroll fleet agent common script utilities --- .../endpoint/common/endpoint_host_services.ts | 20 ++++++++++++- .../scripts/endpoint/common/fleet_services.ts | 29 ++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_host_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_host_services.ts index 4fdb7a7896e25..4bb03324f172e 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_host_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_host_services.ts @@ -14,6 +14,7 @@ import { fetchAgentPolicyEnrollmentKey, fetchFleetServerUrl, getAgentDownloadUrl, + unEnrollFleetAgent, waitForHostToEnroll, } from './fleet_services'; @@ -86,7 +87,20 @@ export const createAndEnrollEndpointHost = async ({ }; }; -// FIXME:PT create function to remove endpoint host +/** + * Destroys the Endpoint Host VM and un-enrolls the Fleet agent + * @param kbnClient + * @param createdHost + */ +export const destroyEndpointHost = async ( + kbnClient: KbnClient, + createdHost: CreateAndEnrollEndpointHostResponse +): Promise => { + await Promise.all([ + deleteMultipassVm(createdHost.hostname), + unEnrollFleetAgent(kbnClient, createdHost.agentId, true), + ]); +}; interface CreateMultipassVmOptions { vmName: string; @@ -120,6 +134,10 @@ const createMultipassVm = async ({ }; }; +const deleteMultipassVm = async (vmName: string): Promise => { + await execa.command(`multipass delete -p ${vmName}`); +}; + interface EnrollHostWithFleetOptions { kbnClient: KbnClient; log: ToolingLog; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts index 73e60667e79fd..fca93d1848f4a 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts @@ -14,7 +14,12 @@ import type { GetAgentPoliciesResponse, GetAgentsResponse, } from '@kbn/fleet-plugin/common'; -import { AGENT_API_ROUTES, agentPolicyRouteService, AGENTS_INDEX } from '@kbn/fleet-plugin/common'; +import { + AGENT_API_ROUTES, + agentPolicyRouteService, + agentRouteService, + AGENTS_INDEX, +} from '@kbn/fleet-plugin/common'; import { ToolingLog } from '@kbn/tooling-log'; import type { KbnClient } from '@kbn/test'; import type { GetFleetServerHostsResponse } from '@kbn/fleet-plugin/common/types/rest_spec/fleet_server_hosts'; @@ -26,6 +31,7 @@ import type { EnrollmentAPIKey, GetAgentsRequest, GetEnrollmentAPIKeysResponse, + PostAgentUnenrollResponse, } from '@kbn/fleet-plugin/common/types'; import nodeFetch from 'node-fetch'; import semver from 'semver'; @@ -349,3 +355,24 @@ export const getLatestAgentDownloadVersion = async ( return stackVersionToArtifactVersion[matchedVersion]; }; + +/** + * Un-enrolls a Fleet agent + * + * @param kbnClient + * @param agentId + * @param force + */ +export const unEnrollFleetAgent = async ( + kbnClient: KbnClient, + agentId: string, + force = false +): Promise => { + const { data } = await kbnClient.request({ + method: 'POST', + path: agentRouteService.getUnenrollPath(agentId), + body: { revoke: force }, + }); + + return data; +}; From 43f035544dd9b17c518b905f8fd3152335ffdd1f Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Thu, 20 Apr 2023 10:52:55 -0400 Subject: [PATCH 19/24] added Delete VM + unenroll agent + delete policies to the test --- .../cypress/e2e/endpoint/endpoint_alerts.cy.ts | 15 +++++++++------ .../management/cypress/support/data_loaders.ts | 12 +++++++++--- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts index a218679a6ae73..e67c579bd0056 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts @@ -53,12 +53,15 @@ describe('Endpoint generated alerts', () => { }); after(() => { - // FIXME:PT implement - // 1. delete VM created - // 2, Force-delete host from fleet (so we can delete policy) - // 3, Removed policy created - // - // + if (createdHost) { + cy.task('destroyEndpointHost', createdHost).then(() => {}); + } + + if (indexedPolicy) { + cy.task('deleteIndexedFleetEndpointPolicies', indexedPolicy); + } + + // FIXME:PT implement additional data deletion // ?. Clean up: // created action // created action response 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 75ce4010a6a0b..a41092a988697 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 @@ -39,7 +39,10 @@ import { deleteIndexedEndpointRuleAlerts, indexEndpointRuleAlerts, } from '../../../../common/endpoint/data_loaders/index_endpoint_rule_alerts'; -import { createAndEnrollEndpointHost } from '../../../../scripts/endpoint/common/endpoint_host_services'; +import { + createAndEnrollEndpointHost, + destroyEndpointHost, +} from '../../../../scripts/endpoint/common/endpoint_host_services'; /** * Cypress plugin for adding data loading related `task`s @@ -173,8 +176,11 @@ export const dataLoadersForRealEndpoints = ( }); }, - destroyEndpointHost: async () => { - // FIXME:PT implement + destroyEndpointHost: async ( + createdHost: CreateAndEnrollEndpointHostResponse + ): Promise => { + const { kbnClient } = await stackServicesPromise; + return destroyEndpointHost(kbnClient, createdHost).then(() => null); }, }); }; From a1b5add36d5c8c3b667e5f4958ca53b87e89293a Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Thu, 20 Apr 2023 14:38:14 -0400 Subject: [PATCH 20/24] Initial work for a `deleteAllEndpointData()` utility --- .../common/delete_all_endpoint_data.ts | 46 +++++++++++++++++++ .../endpoint/common/security_user_services.ts | 31 ++++++++++++- 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/security_solution/scripts/endpoint/common/delete_all_endpoint_data.ts diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/delete_all_endpoint_data.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/delete_all_endpoint_data.ts new file mode 100644 index 0000000000000..1e8df6754c6b2 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/delete_all_endpoint_data.ts @@ -0,0 +1,46 @@ +/* + * 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 { Client } from '@elastic/elasticsearch'; +import { createEsClient } from './stack_services'; +import { createSecuritySuperuser } from './security_user_services'; + +/** + * Attempts to delete all data associated with the provided endpoint agent IDs. + * + * **NOTE:** This utility will create a new role and user that has elevated privileges and access to system indexes. + * + * @param esClient + * @param endpointAgentIds + */ +const deleteAllEndpointData = async (esClient: Client, endpointAgentIds: string[]) => { + const unrestrictedUser = await createSecuritySuperuser(esClient, 'super_superuser'); + const esUrl = getEsUrlFromClient(esClient); + const esClientUnrestricted = createEsClient({ + url: esUrl, + username: unrestrictedUser.username, + password: unrestrictedUser.password, + }); + + // FIXME:PT Implement deleteAllEndpointData +}; + +const getEsUrlFromClient = (esClient: Client) => { + const connection = esClient.connectionPool.connections.find((entry) => entry.status === 'alive'); + + if (!connection) { + throw new Error( + 'Unable to get esClient connection information. No connection found with status `alive`' + ); + } + + const url = new URL(connection.url.href); + url.username = ''; + url.password = ''; + + return url.href; +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/security_user_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/security_user_services.ts index dab9e2b6abd27..f17bf7b514f21 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/security_user_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/security_user_services.ts @@ -17,12 +17,41 @@ export const createSecuritySuperuser = async ( throw new Error(`username and password require values.`); } + // Create a role which has full access to restricted indexes + await esClient.transport.request({ + method: 'POST', + path: '_security/role/superuser_restricted_indices', + body: { + cluster: ['all'], + indices: [ + { + names: ['*'], + privileges: ['all'], + allow_restricted_indices: true, + }, + { + names: ['*'], + privileges: ['monitor', 'read', 'view_index_metadata', 'read_cross_cluster'], + allow_restricted_indices: true, + }, + ], + applications: [ + { + application: '*', + privileges: ['*'], + resources: ['*'], + }, + ], + run_as: ['*'], + }, + }); + const addedUser = await esClient.transport.request>({ method: 'POST', path: `_security/user/${username}`, body: { password, - roles: ['superuser', 'kibana_system'], + roles: ['superuser', 'kibana_system', 'superuser_restricted_indices'], full_name: username, }, }); From a3ac77e7f5803d84f1762d918c634743a73dc8a6 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Thu, 20 Apr 2023 16:16:02 -0400 Subject: [PATCH 21/24] ability to delete all data associated with an Endpoint ID --- .../common/delete_all_endpoint_data.ts | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/delete_all_endpoint_data.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/delete_all_endpoint_data.ts index 1e8df6754c6b2..98bdb80d62ca3 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/delete_all_endpoint_data.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/delete_all_endpoint_data.ts @@ -5,10 +5,16 @@ * 2.0. */ -import type { Client } from '@elastic/elasticsearch'; +import type { Client, estypes } from '@elastic/elasticsearch'; import { createEsClient } from './stack_services'; import { createSecuritySuperuser } from './security_user_services'; +export interface DeleteAllEndpointDataResponse { + count: number; + query: string; + response: estypes.DeleteByQueryResponse; +} + /** * Attempts to delete all data associated with the provided endpoint agent IDs. * @@ -17,7 +23,10 @@ import { createSecuritySuperuser } from './security_user_services'; * @param esClient * @param endpointAgentIds */ -const deleteAllEndpointData = async (esClient: Client, endpointAgentIds: string[]) => { +export const deleteAllEndpointData = async ( + esClient: Client, + endpointAgentIds: string[] +): Promise => { const unrestrictedUser = await createSecuritySuperuser(esClient, 'super_superuser'); const esUrl = getEsUrlFromClient(esClient); const esClientUnrestricted = createEsClient({ @@ -26,7 +35,26 @@ const deleteAllEndpointData = async (esClient: Client, endpointAgentIds: string[ password: unrestrictedUser.password, }); - // FIXME:PT Implement deleteAllEndpointData + const queryString = endpointAgentIds.map((id) => `(${id})`).join(' OR '); + + const deleteResponse = await esClientUnrestricted.deleteByQuery({ + index: '*,.*', + body: { + query: { + query_string: { + query: queryString, + }, + }, + }, + ignore_unavailable: true, + conflicts: 'proceed', + }); + + return { + count: deleteResponse.deleted ?? 0, + query: queryString, + response: deleteResponse, + }; }; const getEsUrlFromClient = (esClient: Client) => { From 9d5d5033604eec4ba3141622e932a0f7f8a8616c Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Thu, 20 Apr 2023 16:41:55 -0400 Subject: [PATCH 22/24] Add deleate all endpoint data task and associated utility function and use it in alerts test --- .../public/management/cypress/cypress.d.ts | 7 +++++++ .../cypress/e2e/endpoint/endpoint_alerts.cy.ts | 16 ++++++++-------- .../management/cypress/support/data_loaders.ts | 11 +++++++++++ .../cypress/tasks/delete_all_endpoint_data.ts | 14 ++++++++++++++ .../endpoint/common/delete_all_endpoint_data.ts | 3 +++ 5 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/cypress/tasks/delete_all_endpoint_data.ts 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 a32de299bf86c..e02b2001c6601 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 @@ -10,6 +10,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { CasePostRequest } from '@kbn/cases-plugin/common/api'; +import type { DeleteAllEndpointDataResponse } from '../../../scripts/endpoint/common/delete_all_endpoint_data'; import type { IndexedEndpointPolicyResponse } from '../../../common/endpoint/data_loaders/index_endpoint_policy_response'; import type { HostPolicyResponse } from '../../../common/endpoint/types'; import type { IndexEndpointHostsCyTaskOptions } from './types'; @@ -129,6 +130,12 @@ declare global { arg: IndexedEndpointPolicyResponse, options?: Partial ): Chainable; + + task( + name: 'deleteAllEndpointData', + arg: { endpointAgentIds: string[] }, + options?: Partial + ): Chainable; } } } diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts index e67c579bd0056..8163e74db17b1 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/endpoint_alerts.cy.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { deleteAllLoadedEndpointData } from '../../tasks/delete_all_endpoint_data'; import { getAlertsTableRows, navigateToAlertsList } from '../../screens/alerts'; import { waitForEndpointAlerts } from '../../tasks/alerts'; import { request } from '../../tasks/common'; @@ -61,12 +62,9 @@ describe('Endpoint generated alerts', () => { cy.task('deleteIndexedFleetEndpointPolicies', indexedPolicy); } - // FIXME:PT implement additional data deletion - // ?. Clean up: - // created action - // created action response - // alerts/events - // files created by potential endpoint actions + if (createdHost) { + deleteAllLoadedEndpointData({ endpointAgentIds: [createdHost.agentId] }); + } }); beforeEach(() => { @@ -74,8 +72,10 @@ describe('Endpoint generated alerts', () => { }); it('should create a Detection Engine alert from an endpoint alert', () => { - // Triggers a Malicious Behaviour alert on Linux system - const executeMaliciousCommand = `bash -c cat /dev/tcp/foo | grep ${createdHost.agentId}`; + // Triggers a Malicious Behaviour alert on Linux system (`grep *` was added only to identify this specific alert) + const executeMaliciousCommand = `bash -c cat /dev/tcp/foo | grep ${Math.random() + .toString(16) + .substring(2)}`; // Send `execute` command that triggers malicious behaviour using the `execute` response action request({ 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 a41092a988697..70afb0c11915e 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 { DeleteAllEndpointDataResponse } from '../../../../scripts/endpoint/common/delete_all_endpoint_data'; +import { deleteAllEndpointData } from '../../../../scripts/endpoint/common/delete_all_endpoint_data'; import { waitForEndpointToStreamData } from '../../../../scripts/endpoint/common/endpoint_metadata_services'; import type { CreateAndEnrollEndpointHostOptions, @@ -149,6 +151,15 @@ export const dataLoaders = ( const { esClient } = await stackServicesPromise; return deleteIndexedEndpointPolicyResponse(esClient, indexedData).then(() => null); }, + + deleteAllEndpointData: async ({ + endpointAgentIds, + }: { + endpointAgentIds: string[]; + }): Promise => { + const { esClient } = await stackServicesPromise; + return deleteAllEndpointData(esClient, endpointAgentIds); + }, }); }; diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/delete_all_endpoint_data.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/delete_all_endpoint_data.ts new file mode 100644 index 0000000000000..761cde513ad52 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/delete_all_endpoint_data.ts @@ -0,0 +1,14 @@ +/* + * 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 { DeleteAllEndpointDataResponse } from '../../../../scripts/endpoint/common/delete_all_endpoint_data'; + +export const deleteAllLoadedEndpointData = (options: { + endpointAgentIds: string[]; +}): Cypress.Chainable => { + return cy.task('deleteAllEndpointData', options); +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/delete_all_endpoint_data.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/delete_all_endpoint_data.ts index 98bdb80d62ca3..6382964fda643 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/delete_all_endpoint_data.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/delete_all_endpoint_data.ts @@ -6,6 +6,7 @@ */ import type { Client, estypes } from '@elastic/elasticsearch'; +import assert from 'assert'; import { createEsClient } from './stack_services'; import { createSecuritySuperuser } from './security_user_services'; @@ -27,6 +28,8 @@ export const deleteAllEndpointData = async ( esClient: Client, endpointAgentIds: string[] ): Promise => { + assert(endpointAgentIds.length > 0, 'At least one endpoint agent id must be defined'); + const unrestrictedUser = await createSecuritySuperuser(esClient, 'super_superuser'); const esUrl = getEsUrlFromClient(esClient); const esClientUnrestricted = createEsClient({ From 9e863b639a295dc0691230b6ed6e452336d89df2 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Mon, 24 Apr 2023 09:17:18 -0400 Subject: [PATCH 23/24] Rename prop of `` --- .../endpoint_agent_status/endpoint_agent_status.test.tsx | 2 +- .../endpoint_agent_status/endpoint_agent_status.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/endpoint_agent_status/endpoint_agent_status.test.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/endpoint_agent_status/endpoint_agent_status.test.tsx index bf6f85712b6c7..7fa169b32d348 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/endpoint_agent_status/endpoint_agent_status.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/endpoint_agent_status/endpoint_agent_status.test.tsx @@ -344,7 +344,7 @@ describe('When showing Endpoint Agent Status', () => { }); it('should keep agent status up to date when autoRefresh is true', async () => { - renderProps.autoFresh = true; + renderProps.autoRefresh = true; apiMocks.responseProvider.metadataDetails.mockReturnValueOnce(endpointDetails); const { getByTestId } = render(); diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/endpoint_agent_status/endpoint_agent_status.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/endpoint_agent_status/endpoint_agent_status.tsx index e74cdcf41fd57..1b2d021634a2f 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/endpoint_agent_status/endpoint_agent_status.tsx +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/endpoint_agent_status/endpoint_agent_status.tsx @@ -138,7 +138,7 @@ export interface EndpointAgentStatusByIdProps { * If set to `true` (Default), then the endpoint status and isolation/action counts will * be kept up to date by querying the API periodically */ - autoFresh?: boolean; + autoRefresh?: boolean; 'data-test-subj'?: string; } @@ -150,9 +150,9 @@ export interface EndpointAgentStatusByIdProps { * instead in order to avoid duplicate API calls. */ export const EndpointAgentStatusById = memo( - ({ endpointAgentId, autoFresh, 'data-test-subj': dataTestSubj }) => { + ({ endpointAgentId, autoRefresh, 'data-test-subj': dataTestSubj }) => { const { data } = useGetEndpointDetails(endpointAgentId, { - refetchInterval: autoFresh ? DEFAULT_POLL_INTERVAL : false, + refetchInterval: autoRefresh ? DEFAULT_POLL_INTERVAL : false, }); const emptyValue = ( @@ -169,7 +169,7 @@ export const EndpointAgentStatusById = memo( ); } From 745b61c222e885fe4b943404bb05891d302a2579 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Mon, 24 Apr 2023 11:14:16 -0400 Subject: [PATCH 24/24] added code to ignore 404 error only in `waitForEndpointToStreamData` --- .../management/cypress/tasks/response_actions.ts | 2 +- .../endpoint/common/endpoint_metadata_services.ts | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts index 62ea4feed7efc..13829f8d3378c 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts @@ -94,7 +94,7 @@ export const waitForActionToComplete = ( ) .then(() => { if (!action) { - throw new Error(`Unable to completed action`); + throw new Error(`Failed to retrieve completed action`); } return action; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_metadata_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_metadata_services.ts index 115dd025ac398..7823309aa0059 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_metadata_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_metadata_services.ts @@ -152,9 +152,14 @@ export const waitForEndpointToStreamData = async ( let found: HostInfo | undefined; while (!found && !hasTimedOut()) { - found = await fetchEndpointMetadata(kbnClient, endpointAgentId).catch((error) => { - // FIXME:PT check errors, ignore 404 - return undefined; + found = await fetchEndpointMetadata(kbnClient, 'invalid-id-test').catch((error) => { + // Ignore `not found` (404) responses. Endpoint could be new and thus documents might not have + // been streamed yet. + if (error?.response?.status === 404) { + return undefined; + } + + throw error; }); if (!found) {