diff --git a/.buildkite/scripts/packer_cache.sh b/.buildkite/scripts/packer_cache.sh index 727accaeead8f..66d663180ec3c 100755 --- a/.buildkite/scripts/packer_cache.sh +++ b/.buildkite/scripts/packer_cache.sh @@ -14,5 +14,9 @@ for version in $(cat versions.json | jq -r '.versions[].version'); do node scripts/es snapshot --download-only --base-path "$ES_CACHE_DIR" --version "$version" done +for version in $(cat versions.json | jq -r '.versions[].version'); do + node x-pack/plugins/security_solution/scripts/endpoint/agent_downloader --version "$version" +done + echo "--- Cloning repos for docs build" node scripts/validate_next_docs --clone-only diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/disabled/uninstall_agent_from_host.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/disabled/uninstall_agent_from_host.cy.ts index 34aba3fcfccf2..ed47855ac894a 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/disabled/uninstall_agent_from_host.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/disabled/uninstall_agent_from_host.cy.ts @@ -21,8 +21,7 @@ import { enableAllPolicyProtections } from '../../../tasks/endpoint_policy'; import { createEndpointHost } from '../../../tasks/create_endpoint_host'; import { deleteAllLoadedEndpointData } from '../../../tasks/delete_all_endpoint_data'; -// FLAKY: https://github.com/elastic/kibana/issues/170667 -describe.skip( +describe( 'Uninstall agent from host when agent tamper protection is disabled', { tags: ['@ess'] }, () => { diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/enabled/uninstall_agent_from_host.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/enabled/uninstall_agent_from_host.cy.ts index 8f45e3d70b5e6..527566bed608b 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/enabled/uninstall_agent_from_host.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/enabled/uninstall_agent_from_host.cy.ts @@ -22,8 +22,7 @@ import { login } from '../../../tasks/login'; import { createEndpointHost } from '../../../tasks/create_endpoint_host'; import { deleteAllLoadedEndpointData } from '../../../tasks/delete_all_endpoint_data'; -// FLAKY: https://github.com/elastic/kibana/issues/170601 -describe.skip( +describe( 'Uninstall agent from host when agent tamper protection is enabled', { tags: ['@ess'] }, () => { diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/switching_policies/uninstall_agent_from_host_changing_policy_from_enabled_to_disabled.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/switching_policies/uninstall_agent_from_host_changing_policy_from_enabled_to_disabled.cy.ts index f5665d830eb4a..0768c4a49ca39 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/switching_policies/uninstall_agent_from_host_changing_policy_from_enabled_to_disabled.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/switching_policies/uninstall_agent_from_host_changing_policy_from_enabled_to_disabled.cy.ts @@ -23,8 +23,7 @@ import { enableAllPolicyProtections } from '../../../tasks/endpoint_policy'; import { createEndpointHost } from '../../../tasks/create_endpoint_host'; import { deleteAllLoadedEndpointData } from '../../../tasks/delete_all_endpoint_data'; -// FLAKY: https://github.com/elastic/kibana/issues/170604 -describe.skip( +describe( 'Uninstall agent from host changing agent policy when agent tamper protection is enabled but then is switched to a policy with it disabled', { tags: ['@ess'] }, () => { diff --git a/x-pack/plugins/security_solution/public/management/cypress/support/create_and_enroll_endpoint_host_ci.ts b/x-pack/plugins/security_solution/public/management/cypress/support/create_and_enroll_endpoint_host_ci.ts new file mode 100644 index 0000000000000..df3a5cf6d38a0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/support/create_and_enroll_endpoint_host_ci.ts @@ -0,0 +1,132 @@ +/* + * 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 { kibanaPackageJson } from '@kbn/repo-info'; +import type { ToolingLog } from '@kbn/tooling-log'; +import type { KbnClient } from '@kbn/test/src/kbn_client'; +import { isFleetServerRunning } from '../../../../scripts/endpoint/common/fleet_server/fleet_server_services'; +import type { HostVm } from '../../../../scripts/endpoint/common/types'; +import type { BaseVmCreateOptions } from '../../../../scripts/endpoint/common/vm_services'; +import { createVm } from '../../../../scripts/endpoint/common/vm_services'; +import { + fetchAgentPolicyEnrollmentKey, + fetchFleetServerUrl, + getAgentDownloadUrl, + getAgentFileName, + getOrCreateDefaultAgentPolicy, + waitForHostToEnroll, +} from '../../../../scripts/endpoint/common/fleet_services'; +import type { DownloadedAgentInfo } from '../../../../scripts/endpoint/common/agent_downloads_service'; +import { + downloadAndStoreAgent, + isAgentDownloadFromDiskAvailable, +} from '../../../../scripts/endpoint/common/agent_downloads_service'; + +export interface CreateAndEnrollEndpointHostCIOptions + 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; + /** If `version` should be exact, or if this is `true`, then the closest version will be used. Defaults to `false` */ + useClosestVersionMatch?: boolean; +} + +export interface CreateAndEnrollEndpointHostCIResponse { + hostname: string; + agentId: string; + hostVm: HostVm; +} + +/** + * Creates a new virtual machine (host) and enrolls that with Fleet + */ +export const createAndEnrollEndpointHostCI = async ({ + kbnClient, + log, + agentPolicyId, + cpus, + disk, + memory, + hostname, + version = kibanaPackageJson.version, + useClosestVersionMatch = true, +}: CreateAndEnrollEndpointHostCIOptions): Promise => { + const vmName = hostname ?? `test-host-${Math.random().toString().substring(2, 6)}`; + + const fileNameNoExtension = getAgentFileName(version); + const agentFileName = `${fileNameNoExtension}.tar.gz`; + let agentDownload: DownloadedAgentInfo | undefined; + + // Check if agent file is already on disk before downloading it again + agentDownload = isAgentDownloadFromDiskAvailable(agentFileName); + + // If it has not been already downloaded, it should be downloaded. + if (!agentDownload) { + log.warning( + `There is no agent installer for ${agentFileName} present on disk, trying to download it now.` + ); + const { url: agentUrl } = await getAgentDownloadUrl(version, useClosestVersionMatch, log); + agentDownload = await downloadAndStoreAgent(agentUrl, agentFileName); + } + + const hostVm = await createVm({ + type: 'vagrant', + name: vmName, + log, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + agentDownload: agentDownload!, + disk, + cpus, + memory, + }); + + if (!(await isFleetServerRunning(kbnClient))) { + throw new Error(`Fleet server does not seem to be running on this instance of kibana!`); + } + + const policyId = agentPolicyId || (await getOrCreateDefaultAgentPolicy({ kbnClient, log })).id; + const [fleetServerUrl, enrollmentToken] = await Promise.all([ + fetchFleetServerUrl(kbnClient), + fetchAgentPolicyEnrollmentKey(kbnClient, policyId), + ]); + + const agentEnrollCommand = [ + 'sudo', + + `./${fileNameNoExtension}/elastic-agent`, + + 'install', + + '--insecure', + + '--force', + + '--url', + fleetServerUrl, + + '--enrollment-token', + enrollmentToken, + ].join(' '); + + log.info(`Enrolling Elastic Agent with Fleet`); + log.verbose('Enrollment command:', agentEnrollCommand); + + await hostVm.exec(agentEnrollCommand); + + const { id: agentId } = await waitForHostToEnroll(kbnClient, log, hostVm.name, 240000); + + return { + hostname: hostVm.name, + agentId, + hostVm, + }; +}; 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 99ea877053c91..299d210a65089 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 @@ -26,10 +26,7 @@ import { 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, - CreateAndEnrollEndpointHostResponse, -} from '../../../../scripts/endpoint/common/endpoint_host_services'; +import type { CreateAndEnrollEndpointHostResponse } from '../../../../scripts/endpoint/common/endpoint_host_services'; import { createAndEnrollEndpointHost, destroyEndpointHost, @@ -66,6 +63,11 @@ import { indexFleetEndpointPolicy, } from '../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy'; import { cyLoadEndpointDataHandler } from './plugin_handlers/endpoint_data_loader'; +import type { + CreateAndEnrollEndpointHostCIOptions, + CreateAndEnrollEndpointHostCIResponse, +} from './create_and_enroll_endpoint_host_ci'; +import { createAndEnrollEndpointHostCI } from './create_and_enroll_endpoint_host_ci'; /** * Test Role/User loader for cypress. Checks to see if running in serverless and handles it as appropriate @@ -290,40 +292,48 @@ export const dataLoadersForRealEndpoints = ( on('task', { createEndpointHost: async ( - options: Omit - ): Promise => { + options: Omit + ): Promise => { const { kbnClient, log } = await stackServicesPromise; let retryAttempt = 0; - const attemptCreateEndpointHost = async (): Promise => { - try { - log.info(`Creating endpoint host, attempt ${retryAttempt}`); - const newHost = await createAndEnrollEndpointHost({ - useClosestVersionMatch: true, - ...options, - log, - kbnClient, - }); - await waitForEndpointToStreamData(kbnClient, newHost.agentId, 360000); - return newHost; - } catch (err) { - log.info(`Caught error when setting up the agent: ${err}`); - if (retryAttempt === 0 && err.agentId) { - retryAttempt++; - await destroyEndpointHost(kbnClient, { - hostname: err.hostname || '', // No hostname in CI env for vagrant - agentId: err.agentId, - }); - log.info(`Deleted endpoint host ${err.agentId} and retrying`); - return attemptCreateEndpointHost(); - } else { - log.info( - `${retryAttempt} attempts of creating endpoint host failed, reason for the last failure was ${err}` - ); - throw err; + const attemptCreateEndpointHost = + async (): Promise => { + try { + log.info(`Creating endpoint host, attempt ${retryAttempt}`); + const newHost = process.env.CI + ? await createAndEnrollEndpointHostCI({ + useClosestVersionMatch: true, + ...options, + log, + kbnClient, + }) + : await createAndEnrollEndpointHost({ + useClosestVersionMatch: true, + ...options, + log, + kbnClient, + }); + await waitForEndpointToStreamData(kbnClient, newHost.agentId, 360000); + return newHost; + } catch (err) { + log.info(`Caught error when setting up the agent: ${err}`); + if (retryAttempt === 0 && err.agentId) { + retryAttempt++; + await destroyEndpointHost(kbnClient, { + hostname: err.hostname || '', // No hostname in CI env for vagrant + agentId: err.agentId, + }); + log.info(`Deleted endpoint host ${err.agentId} and retrying`); + return attemptCreateEndpointHost(); + } else { + log.info( + `${retryAttempt} attempts of creating endpoint host failed, reason for the last failure was ${err}` + ); + throw err; + } } - } - }; + }; return attemptCreateEndpointHost(); }, diff --git a/x-pack/plugins/security_solution/scripts/endpoint/agent_downloader.js b/x-pack/plugins/security_solution/scripts/endpoint/agent_downloader.js new file mode 100644 index 0000000000000..050be8e0cf071 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/agent_downloader.js @@ -0,0 +1,9 @@ +/* + * 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. + */ + +require('../../../../../src/setup_node_env'); +require('./agent_downloader_cli').cli(); diff --git a/x-pack/plugins/security_solution/scripts/endpoint/agent_downloader_cli/agent_downloader.ts b/x-pack/plugins/security_solution/scripts/endpoint/agent_downloader_cli/agent_downloader.ts new file mode 100644 index 0000000000000..ab1da6a3f208f --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/agent_downloader_cli/agent_downloader.ts @@ -0,0 +1,32 @@ +/* + * 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 { ok } from 'assert'; +import type { RunFn } from '@kbn/dev-cli-runner'; +import type { ToolingLog } from '@kbn/tooling-log'; +import { getAgentDownloadUrl, getAgentFileName } from '../common/fleet_services'; +import { downloadAndStoreAgent } from '../common/agent_downloads_service'; + +const downloadAndStoreElasticAgent = async ( + version: string, + closestMatch: boolean, + log: ToolingLog +) => { + const downloadUrlResponse = await getAgentDownloadUrl(version, closestMatch, log); + const fileNameNoExtension = getAgentFileName(version); + const agentFile = `${fileNameNoExtension}.tar.gz`; + await downloadAndStoreAgent(downloadUrlResponse.url, agentFile); +}; + +export const agentDownloaderRunner: RunFn = async (cliContext) => { + ok(cliContext.flags.version, 'version argument is required'); + await downloadAndStoreElasticAgent( + cliContext.flags.version as string, + cliContext.flags.closestMatch as boolean, + cliContext.log + ); +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/agent_downloader_cli/index.ts b/x-pack/plugins/security_solution/scripts/endpoint/agent_downloader_cli/index.ts new file mode 100644 index 0000000000000..7cc6988b63de8 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/agent_downloader_cli/index.ts @@ -0,0 +1,31 @@ +/* + * 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 { run } from '@kbn/dev-cli-runner'; +import { agentDownloaderRunner } from './agent_downloader'; + +export const cli = () => { + run( + agentDownloaderRunner, + + // Options + { + description: `Elastic Agent downloader`, + flags: { + string: ['version'], + boolean: ['closestMatch'], + default: { + closestMatch: true, + }, + help: ` + --version Required. Elastic agent version to be downloaded. + --closestMatch Optional. Use closest elastic agent version to match with. + `, + }, + } + ); +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/agent_downloads_service.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/agent_downloads_service.ts index 34f473a854460..410ddd65cf842 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/agent_downloads_service.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/agent_downloads_service.ts @@ -64,8 +64,10 @@ class AgentDownloadStorage extends SettingsStorage } } - public getPathsForUrl(agentDownloadUrl: string): DownloadedAgentInfo { - const filename = agentDownloadUrl.replace(/^https?:\/\//gi, '').replace(/\//g, '#'); + public getPathsForUrl(agentDownloadUrl: string, agentFileName?: string): DownloadedAgentInfo { + const filename = agentFileName + ? agentFileName + : agentDownloadUrl.replace(/^https?:\/\//gi, '').replace(/\//g, '#'); const directory = this.downloadsDirFullPath; const fullFilePath = this.buildPath(join(this.downloadsDirName, filename)); @@ -76,14 +78,17 @@ class AgentDownloadStorage extends SettingsStorage }; } - public async downloadAndStore(agentDownloadUrl: string): Promise { + public async downloadAndStore( + agentDownloadUrl: string, + agentFileName?: string + ): Promise { this.log.debug(`Downloading and storing: ${agentDownloadUrl}`); // TODO: should we add "retry" attempts to file downloads? await this.ensureExists(); - const newDownloadInfo = this.getPathsForUrl(agentDownloadUrl); + const newDownloadInfo = this.getPathsForUrl(agentDownloadUrl, agentFileName); // If download is already present on disk, then just return that info. No need to re-download it if (fs.existsSync(newDownloadInfo.fullFilePath)) { @@ -154,6 +159,18 @@ class AgentDownloadStorage extends SettingsStorage return response; } + + public isAgentDownloadFromDiskAvailable(filename: string): DownloadedAgentInfo | undefined { + if (fs.existsSync(join(this.downloadsDirFullPath, filename))) { + return { + filename, + /** The local directory where downloads are stored */ + directory: this.downloadsDirFullPath, + /** The full local file path and name */ + fullFilePath: join(this.downloadsDirFullPath, filename), + }; + } + } } const handleProcessInterruptions = async ( @@ -203,11 +220,16 @@ export interface DownloadAndStoreAgentResponse extends DownloadedAgentInfo { * already exists on disk, then no download is actually done - the information about the cached * version is returned instead * @param agentDownloadUrl + * @param agentFileName */ export const downloadAndStoreAgent = async ( - agentDownloadUrl: string + agentDownloadUrl: string, + agentFileName?: string ): Promise => { - const downloadedAgent = await agentDownloadsClient.downloadAndStore(agentDownloadUrl); + const downloadedAgent = await agentDownloadsClient.downloadAndStore( + agentDownloadUrl, + agentFileName + ); return { url: agentDownloadUrl, @@ -221,3 +243,9 @@ export const downloadAndStoreAgent = async ( export const cleanupDownloads = async (): ReturnType => { return agentDownloadsClient.cleanupDownloads(); }; + +export const isAgentDownloadFromDiskAvailable = ( + fileName: string +): DownloadedAgentInfo | undefined => { + return agentDownloadsClient.isAgentDownloadFromDiskAvailable(fileName); +}; 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 26ca9d6474393..6477bfc3bebeb 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 @@ -373,6 +373,16 @@ export const getAgentVersionMatchingCurrentStack = async ( return version; }; +// Generates a file name using system arch and an agent version. +export const getAgentFileName = (agentVersion: string): string => { + const downloadArch = + { arm64: 'arm64', x64: 'x86_64' }[process.arch as string] ?? + `UNSUPPORTED_ARCHITECTURE_${process.arch}`; + const fileName = `elastic-agent-${agentVersion}-linux-${downloadArch}`; + + return fileName; +}; + interface ElasticArtifactSearchResponse { manifest: { 'last-update-time': string; @@ -414,11 +424,9 @@ export const getAgentDownloadUrl = async ( log?: ToolingLog ): Promise => { const agentVersion = closestMatch ? await getLatestAgentDownloadVersion(version, log) : version; - const downloadArch = - { arm64: 'arm64', x64: 'x86_64' }[process.arch as string] ?? - `UNSUPPORTED_ARCHITECTURE_${process.arch}`; - const fileNameNoExtension = `elastic-agent-${agentVersion}-linux-${downloadArch}`; - const agentFile = `${fileNameNoExtension}.tar.gz`; + + const fileNameWithoutExtension = getAgentFileName(agentVersion); + const agentFile = `${fileNameWithoutExtension}.tar.gz`; const artifactSearchUrl = `https://artifacts-api.elastic.co/v1/search/${agentVersion}/${agentFile}`; log?.verbose(`Retrieving elastic agent download URL from:\n ${artifactSearchUrl}`); @@ -444,7 +452,7 @@ export const getAgentDownloadUrl = async ( return { url: searchResult.packages[agentFile].url, fileName: agentFile, - dirName: fileNameNoExtension, + dirName: fileNameWithoutExtension, }; }; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/vagrant/Vagrantfile b/x-pack/plugins/security_solution/scripts/endpoint/common/vagrant/Vagrantfile index fd33efaeab625..b058d17ad0764 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/vagrant/Vagrantfile +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/vagrant/Vagrantfile @@ -5,6 +5,7 @@ hostname = ENV["VMNAME"] || 'ubuntu' cachedAgentSource = ENV["CACHED_AGENT_SOURCE"] || '' cachedAgentFilename = ENV["CACHED_AGENT_FILENAME"] || '' +agentDestinationFolder = ENV["AGENT_DESTINATION_FOLDER"] || '' Vagrant.configure("2") do |config| config.vm.hostname = hostname @@ -29,6 +30,7 @@ Vagrant.configure("2") do |config| end config.vm.provision "file", source: cachedAgentSource, destination: "~/#{cachedAgentFilename}" - config.vm.provision "shell", inline: "tar -zxf #{cachedAgentFilename} && rm -f #{cachedAgentFilename}" + config.vm.provision "shell", inline: "mkdir #{agentDestinationFolder}" + config.vm.provision "shell", inline: "tar -zxf #{cachedAgentFilename} --directory #{agentDestinationFolder} --strip-components=1 && rm -f #{cachedAgentFilename}" config.vm.provision "shell", inline: "sudo apt-get install unzip" end diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/vm_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/vm_services.ts index a0efd908f80d7..17c74b1bf6fc3 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/vm_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/vm_services.ts @@ -249,6 +249,7 @@ const createVagrantVm = async ({ VMNAME: name, CACHED_AGENT_SOURCE: agentFullFilePath, CACHED_AGENT_FILENAME: agentFileName, + AGENT_DESTINATION_FOLDER: agentFileName.replace('.tar.gz', ''), }, // Only `pipe` STDERR to parent process stdio: ['inherit', 'inherit', 'pipe'],