From 8613b0f17d228fdaa3c9e3d1e79979ae82b7a981 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Mon, 13 Nov 2023 15:10:55 -0500 Subject: [PATCH] [Security Solution][Endpoint] several refactors of CLI tooling and associated common services (#169987) ## Summary PR makes a series of refactors to CLI scripts and common services used in CLI scripts and CI, including: - Standard interface for interacting with Host VMs that abstracts away the need to know what VM manager was used to start that VM - Reduce/eliminate the need to have conditional code when interacting directly with a VM (ex. executing bash commands, stop/kill/delete VM, etc) - Removed use of `endpoint_agent_runner` (CLI script) private implementation methods from Cypress and replace them with calls to common services - Removed duplicate code from `endpoint_agent_runner` CLI script and replace it with calls to common services - Enhanced the `run_sentinelone_host.js` script so that it also ensures that the SentinenlOne fleet integration/policy (agentless policy) has at least one VM host running - The VM ensures that the data from S1 is pulled into ES - FYI: once changes for SentinelOne are merged and the Connector available, script will also be updated to create an SentinelOne connector instance under `"Stack Management > Connectors"` - Added support for `WITH_FLEET_SERVER` to the Cypress config. When set to `true`, fleet server will be automatically started and connected to the stack - Cypress parallel runner will now start fleet if this variable is true, right after setting up the stack --- src/dev/precommit_hook/casing_check_config.js | 2 +- .../common/endpoint/data_loaders/utils.ts | 24 +- .../public/management/cypress/cypress.d.ts | 11 +- .../management/cypress/cypress_base.config.ts | 8 +- ...ging_policy_from_disabled_to_enabled.cy.ts | 7 +- ...ging_policy_from_enabled_to_disabled.cy.ts | 4 +- ...nging_policy_from_enabled_to_enabled.cy.ts | 7 +- ...ging_policy_from_disabled_to_enabled.cy.ts | 7 +- ...ging_policy_from_enabled_to_disabled.cy.ts | 4 +- ...nging_policy_from_enabled_to_enabled.cy.ts | 7 +- .../cypress/support/agent_actions.ts | 60 +-- .../management/cypress/support/common.ts | 39 ++ .../cypress/support/data_loaders.ts | 119 +--- .../public/management/cypress/support/e2e.ts | 6 +- .../cypress/support/response_actions.ts | 11 +- .../support/setup_tooling_log_level.ts | 7 +- .../public/management/cypress/tasks/fleet.ts | 59 +- .../public/management/cypress/tasks/logger.ts | 19 + .../public/management/cypress/types.ts | 6 + .../common/agent_downloads_service.ts | 13 + .../endpoint/common/endpoint_host_services.ts | 350 ++---------- .../fleet_server/fleet_server_services.ts | 108 ++-- .../scripts/endpoint/common/fleet_services.ts | 115 ++-- .../scripts/endpoint/common/types.ts | 10 + .../scripts/endpoint/common/utils.ts | 63 +++ .../vagrant}/Vagrantfile | 4 + .../scripts/endpoint/common/vm_services.ts | 280 +++++++++- .../endpoint_agent_runner/elastic_endpoint.ts | 51 +- .../endpoint_agent_runner/fleet_server.ts | 506 ------------------ .../endpoint/endpoint_agent_runner/index.ts | 2 - .../endpoint/endpoint_agent_runner/setup.ts | 11 +- .../endpoint/sentinelone_host/index.ts | 107 ++-- .../scripts/run_cypress/parallel.ts | 107 ++-- x-pack/test/osquery_cypress/agent.ts | 18 +- x-pack/test/osquery_cypress/fleet_server.ts | 40 +- x-pack/test/osquery_cypress/runner.ts | 37 +- 36 files changed, 975 insertions(+), 1254 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/cypress/support/common.ts create mode 100644 x-pack/plugins/security_solution/public/management/cypress/tasks/logger.ts create mode 100644 x-pack/plugins/security_solution/scripts/endpoint/common/utils.ts rename x-pack/plugins/security_solution/scripts/endpoint/{endpoint_agent_runner => common/vagrant}/Vagrantfile (76%) delete mode 100644 x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/fleet_server.ts diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 93eba1bb171b..acf474805558 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -44,7 +44,7 @@ export const IGNORE_FILE_GLOBS = [ 'packages/kbn-test/jest-preset.js', 'packages/kbn-test/*/jest-preset.js', 'test/package/Vagrantfile', - 'x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/Vagrantfile', + 'x-pack/plugins/security_solution/scripts/endpoint/common/vagrant/Vagrantfile', '**/test/**/fixtures/**/*', // Required to match the name in the docs.elastic.dev repo. diff --git a/x-pack/plugins/security_solution/common/endpoint/data_loaders/utils.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/utils.ts index c27fb5fc7154..0586aca1d0c7 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/utils.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/utils.ts @@ -8,6 +8,7 @@ import { mergeWith } from 'lodash'; import type { ToolingLogTextWriterConfig } from '@kbn/tooling-log'; import { ToolingLog } from '@kbn/tooling-log'; +import type { Flags } from '@kbn/dev-cli-runner'; export const RETRYABLE_TRANSIENT_ERRORS: Readonly> = [ 'no_shard_available_action_exception', @@ -117,12 +118,20 @@ interface CreateLoggerInterface { * on input. */ defaultLogLevel: ToolingLogTextWriterConfig['level']; + + /** + * Set the default logging level based on the flag arguments provide to a CLI script that runs + * via `@kbn/dev-cli-runner` + * @param flags + */ + setDefaultLogLevelFromCliFlags: (flags: Flags) => void; } /** * Creates an instance of `ToolingLog` that outputs to `stdout`. - * The default log `level` for all instances can be set by setting the function's `defaultLogLevel`. - * Log level can also be explicitly set on input. + * The default log `level` for all instances can be set by setting the function's `defaultLogLevel` + * property. Default logging level can also be set from CLI scripts that use the `@kbn/dev-cli-runner` + * by calling the `setDefaultLogLevelFromCliFlags(flags)` and passing in the `flags` property. * * @param level * @@ -137,3 +146,14 @@ export const createToolingLogger: CreateLoggerInterface = (level): ToolingLog => }); }; createToolingLogger.defaultLogLevel = 'info'; +createToolingLogger.setDefaultLogLevelFromCliFlags = (flags) => { + createToolingLogger.defaultLogLevel = flags.verbose + ? 'verbose' + : flags.debug + ? 'debug' + : flags.silent + ? 'silent' + : flags.quiet + ? 'error' + : 'info'; +}; 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 bd4c34b36de5..0ae9db14cbce 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 @@ -24,6 +24,7 @@ import type { CreateUserAndRoleCyTaskOptions, UninstallAgentFromHostTaskOptions, IsAgentAndEndpointUninstalledFromHostTaskOptions, + LogItTaskOptions, } from './types'; import type { DeleteIndexedFleetEndpointPoliciesResponse, @@ -86,13 +87,15 @@ declare global { * or fail if `timeout` is reached. * @param fn * @param options + * @param message */ waitUntil( fn: (subject?: any) => boolean | Promise | Chainable, options?: Partial<{ interval: number; timeout: number; - }> + }>, + message?: string ): Chainable; task( @@ -217,6 +220,12 @@ declare global { arg: IsAgentAndEndpointUninstalledFromHostTaskOptions, options?: Partial ): Chainable; + + task( + name: 'logIt', + arg: LogItTaskOptions, + options?: Partial + ): Chainable; } } } diff --git a/x-pack/plugins/security_solution/public/management/cypress/cypress_base.config.ts b/x-pack/plugins/security_solution/public/management/cypress/cypress_base.config.ts index 8938f1a4ec7b..6ed65f031d71 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/cypress_base.config.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/cypress_base.config.ts @@ -56,6 +56,11 @@ export const getCypressBaseConfig = ( // to `debug` or `verbose` when wanting to debug tooling used by tests (ex. data indexer functions). TOOLING_LOG_LEVEL: 'info', + // Variable works in conjunction with the Cypress parallel runner. When set to true, fleet server + // will be setup right after the Kibana stack, so that by the time cypress tests `.run()`/`.open()`, + // the env. will be all setup and we don't have to explicitly setup fleet from a test file + WITH_FLEET_SERVER: true, + // grep related configs grepFilterSpecs: true, grepOmitFiltered: true, @@ -69,11 +74,12 @@ export const getCypressBaseConfig = ( experimentalRunAllSpecs: true, experimentalMemoryManagement: true, experimentalInteractiveRunEvents: true, - setupNodeEvents: (on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions) => { + setupNodeEvents: async (on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions) => { // IMPORTANT: setting the log level should happen before any tooling is called setupToolingLogLevel(config); dataLoaders(on, config); + // Data loaders specific to "real" Endpoint testing dataLoadersForRealEndpoints(on, config); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/switching_policies/unenroll_agent_from_fleet_changing_policy_from_disabled_to_enabled.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/switching_policies/unenroll_agent_from_fleet_changing_policy_from_disabled_to_enabled.cy.ts index d30345d8d548..3d92528c2eee 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/switching_policies/unenroll_agent_from_fleet_changing_policy_from_disabled_to_enabled.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/switching_policies/unenroll_agent_from_fleet_changing_policy_from_disabled_to_enabled.cy.ts @@ -14,7 +14,7 @@ import { createAgentPolicyTask, enableAgentTamperProtectionFeatureFlagInPolicy, unenrollAgent, - changeAgentPolicy, + reAssignFleetAgentToPolicy, } from '../../../tasks/fleet'; import { login } from '../../../tasks/login'; @@ -79,10 +79,9 @@ describe( it('should unenroll from fleet without issues', () => { waitForEndpointListPageToBeLoaded(createdHost.hostname); // Change agent policy and wait for action to be completed - changeAgentPolicy( + reAssignFleetAgentToPolicy( createdHost.agentId, - policyWithAgentTamperProtectionEnabled.policy_id, - 3 + policyWithAgentTamperProtectionEnabled.policy_id ).then((hasChanged) => { expect(hasChanged).to.eql(true); unenrollAgent(createdHost.agentId).then((isUnenrolled) => { diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/switching_policies/unenroll_agent_from_fleet_changing_policy_from_enabled_to_disabled.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/switching_policies/unenroll_agent_from_fleet_changing_policy_from_enabled_to_disabled.cy.ts index 59c70d85118d..a9508a13f719 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/switching_policies/unenroll_agent_from_fleet_changing_policy_from_enabled_to_disabled.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/switching_policies/unenroll_agent_from_fleet_changing_policy_from_enabled_to_disabled.cy.ts @@ -14,7 +14,7 @@ import { createAgentPolicyTask, enableAgentTamperProtectionFeatureFlagInPolicy, unenrollAgent, - changeAgentPolicy, + reAssignFleetAgentToPolicy, } from '../../../tasks/fleet'; import { login } from '../../../tasks/login'; @@ -79,7 +79,7 @@ describe( it('should unenroll from fleet without issues', () => { waitForEndpointListPageToBeLoaded(createdHost.hostname); // Change agent policy and wait for action to be completed - changeAgentPolicy(createdHost.agentId, policy.policy_id, 3).then((hasChanged) => { + reAssignFleetAgentToPolicy(createdHost.agentId, policy.policy_id).then((hasChanged) => { expect(hasChanged).to.eql(true); unenrollAgent(createdHost.agentId).then((isUnenrolled) => { expect(isUnenrolled).to.eql(true); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/switching_policies/unenroll_agent_from_fleet_changing_policy_from_enabled_to_enabled.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/switching_policies/unenroll_agent_from_fleet_changing_policy_from_enabled_to_enabled.cy.ts index 3a897bc544ea..a5654734c15e 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/switching_policies/unenroll_agent_from_fleet_changing_policy_from_enabled_to_enabled.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/switching_policies/unenroll_agent_from_fleet_changing_policy_from_enabled_to_enabled.cy.ts @@ -14,7 +14,7 @@ import { createAgentPolicyTask, enableAgentTamperProtectionFeatureFlagInPolicy, unenrollAgent, - changeAgentPolicy, + reAssignFleetAgentToPolicy, } from '../../../tasks/fleet'; import { login } from '../../../tasks/login'; @@ -81,10 +81,9 @@ describe( it('should unenroll from fleet without issues', () => { waitForEndpointListPageToBeLoaded(createdHost.hostname); // Change agent policy and wait for action to be completed - changeAgentPolicy( + reAssignFleetAgentToPolicy( createdHost.agentId, - secondPolicyWithAgentTamperProtectionEnabled.policy_id, - 3 + secondPolicyWithAgentTamperProtectionEnabled.policy_id ).then((hasChanged) => { expect(hasChanged).to.eql(true); unenrollAgent(createdHost.agentId).then((isUnenrolled) => { diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/switching_policies/uninstall_agent_from_host_changing_policy_from_disabled_to_enabled.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/switching_policies/uninstall_agent_from_host_changing_policy_from_disabled_to_enabled.cy.ts index 5950288f2313..bbb675cf56d5 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/switching_policies/uninstall_agent_from_host_changing_policy_from_disabled_to_enabled.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/switching_policies/uninstall_agent_from_host_changing_policy_from_disabled_to_enabled.cy.ts @@ -14,7 +14,7 @@ import { createAgentPolicyTask, enableAgentTamperProtectionFeatureFlagInPolicy, getUninstallToken, - changeAgentPolicy, + reAssignFleetAgentToPolicy, isAgentAndEndpointUninstalledFromHost, uninstallAgentFromHost, } from '../../../tasks/fleet'; @@ -82,10 +82,9 @@ describe( waitForEndpointListPageToBeLoaded(createdHost.hostname); // Change agent policy and wait for action to be completed - changeAgentPolicy( + reAssignFleetAgentToPolicy( createdHost.agentId, - policyWithAgentTamperProtectionEnabled.policy_id, - 3 + policyWithAgentTamperProtectionEnabled.policy_id ).then((hasChanged) => { expect(hasChanged).to.eql(true); 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 e949851677f2..f5665d830eb4 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 @@ -13,7 +13,7 @@ import { getEndpointIntegrationVersion, createAgentPolicyTask, enableAgentTamperProtectionFeatureFlagInPolicy, - changeAgentPolicy, + reAssignFleetAgentToPolicy, isAgentAndEndpointUninstalledFromHost, uninstallAgentFromHost, } from '../../../tasks/fleet'; @@ -81,7 +81,7 @@ describe.skip( it('should uninstall from host without issues', () => { waitForEndpointListPageToBeLoaded(createdHost.hostname); - changeAgentPolicy(createdHost.agentId, policy.policy_id, 3).then((hasChanged) => { + reAssignFleetAgentToPolicy(createdHost.agentId, policy.policy_id).then((hasChanged) => { expect(hasChanged).to.eql(true); uninstallAgentFromHost(createdHost.hostname).then((responseWithoutToken) => { expect(responseWithoutToken).to.not.match(/(.*)Invalid uninstall token(.*)/); 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_enabled.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_enabled.cy.ts index 15fd02ad1451..d8630a50a83b 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/tamper_protection/switching_policies/uninstall_agent_from_host_changing_policy_from_enabled_to_enabled.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_enabled.cy.ts @@ -14,7 +14,7 @@ import { createAgentPolicyTask, enableAgentTamperProtectionFeatureFlagInPolicy, getUninstallToken, - changeAgentPolicy, + reAssignFleetAgentToPolicy, isAgentAndEndpointUninstalledFromHost, uninstallAgentFromHost, } from '../../../tasks/fleet'; @@ -85,10 +85,9 @@ describe( waitForEndpointListPageToBeLoaded(createdHost.hostname); // Change agent policy and wait for action to be completed - changeAgentPolicy( + reAssignFleetAgentToPolicy( createdHost.agentId, - secondPolicyWithAgentTamperProtectionEnabled.policy_id, - 3 + secondPolicyWithAgentTamperProtectionEnabled.policy_id ).then((hasChanged) => { expect(hasChanged).to.eql(true); diff --git a/x-pack/plugins/security_solution/public/management/cypress/support/agent_actions.ts b/x-pack/plugins/security_solution/public/management/cypress/support/agent_actions.ts index 11a8b30a5e18..5ad564aaa14b 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/support/agent_actions.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/support/agent_actions.ts @@ -6,10 +6,8 @@ */ // / -import type { ExecaReturnValue } from 'execa'; -import execa from 'execa'; -import { VAGRANT_CWD } from '../../../../scripts/endpoint/common/endpoint_host_services'; +import { getHostVmClient } from '../../../../scripts/endpoint/common/vm_services'; export const agentActions = (on: Cypress.PluginEvents): void => { on('task', { @@ -20,40 +18,19 @@ export const agentActions = (on: Cypress.PluginEvents): void => { hostname: string; uninstallToken?: string; }): Promise => { - let result; + const hostVmClient = getHostVmClient(hostname); + try { - if (process.env.CI) { - result = await execa( - 'vagrant', - [ - 'ssh', - '--', - `sudo elastic-agent uninstall -f ${ - uninstallToken ? `--uninstall-token ${uninstallToken}` : '' - }`, - ], - { - env: { - VAGRANT_CWD, - }, - } - ); - } else { - result = await execa(`multipass`, [ - 'exec', - hostname, - '--', - 'sh', - '-c', + return ( + await hostVmClient.exec( `sudo elastic-agent uninstall -f ${ uninstallToken ? `--uninstall-token ${uninstallToken}` : '' - }`, - ]); - } + }` + ) + ).stdout; } catch (err) { return err.stderr; } - return result.stdout; }, isAgentAndEndpointUninstalledFromHost: async ({ @@ -62,25 +39,10 @@ export const agentActions = (on: Cypress.PluginEvents): void => { hostname: string; uninstallToken?: string; }): Promise => { - let execaReturnValue: ExecaReturnValue; - if (process.env.CI) { - execaReturnValue = await execa('vagrant', ['ssh', '--', `ls /opt/Elastic`], { - env: { - VAGRANT_CWD, - }, - }); - } else { - execaReturnValue = await execa(`multipass`, [ - 'exec', - hostname, - '--', - 'sh', - '-c', - `ls /opt/Elastic`, - ]); - } + const hostVmClient = getHostVmClient(hostname); + const lsOutput = await hostVmClient.exec('ls /opt/Elastic'); - if (execaReturnValue.stdout === '') { + if (lsOutput.stdout === '') { return true; } diff --git a/x-pack/plugins/security_solution/public/management/cypress/support/common.ts b/x-pack/plugins/security_solution/public/management/cypress/support/common.ts new file mode 100644 index 000000000000..c356536cc03d --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/support/common.ts @@ -0,0 +1,39 @@ +/* + * 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 { prefixedOutputLogger } from '../../../../scripts/endpoint/common/utils'; +import type { RuntimeServices } from '../../../../scripts/endpoint/common/stack_services'; +import { createRuntimeServices } from '../../../../scripts/endpoint/common/stack_services'; + +const RUNTIME_SERVICES_CACHE = new WeakMap(); + +export const setupStackServicesUsingCypressConfig = async (config: Cypress.PluginConfigOptions) => { + if (RUNTIME_SERVICES_CACHE.has(config)) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return RUNTIME_SERVICES_CACHE.get(config)!; + } + + const stackServices = await createRuntimeServices({ + kibanaUrl: config.env.KIBANA_URL, + elasticsearchUrl: config.env.ELASTICSEARCH_URL, + fleetServerUrl: config.env.FLEET_SERVER_URL, + username: config.env.KIBANA_USERNAME, + password: config.env.KIBANA_PASSWORD, + esUsername: config.env.ELASTICSEARCH_USERNAME, + esPassword: config.env.ELASTICSEARCH_PASSWORD, + asSuperuser: true, + }).then(({ log, ...others }) => { + return { + ...others, + log: prefixedOutputLogger('cy.dfw', log), + }; + }); + + RUNTIME_SERVICES_CACHE.set(config, stackServices); + + return stackServices; +}; 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 fcead968801a..99ea877053c9 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 @@ -11,14 +11,14 @@ import type { CasePostRequest } from '@kbn/cases-plugin/common'; import execa from 'execa'; import type { KbnClient } from '@kbn/test'; import type { ToolingLog } from '@kbn/tooling-log'; +import { getHostVmClient } from '../../../../scripts/endpoint/common/vm_services'; +import { setupStackServicesUsingCypressConfig } from './common'; import type { KibanaKnownUserAccounts } from '../common/constants'; import { KIBANA_KNOWN_DEFAULT_ACCOUNTS } from '../common/constants'; import type { EndpointSecurityRoleNames } from '../../../../scripts/endpoint/common/roles_users'; import { SECURITY_SERVERLESS_ROLE_NAMES } from '../../../../scripts/endpoint/common/roles_users'; import type { LoadedRoleAndUser } from '../../../../scripts/endpoint/common/role_and_user_loader'; import { EndpointSecurityTestRolesLoader } from '../../../../scripts/endpoint/common/role_and_user_loader'; -import { startRuntimeServices } from '../../../../scripts/endpoint/endpoint_agent_runner/runtime'; -import { runFleetServerIfNeeded } from '../../../../scripts/endpoint/endpoint_agent_runner/fleet_server'; import { sendEndpointActionResponse, sendFleetActionResponse, @@ -35,7 +35,6 @@ import { destroyEndpointHost, startEndpointHost, stopEndpointHost, - VAGRANT_CWD, } from '../../../../scripts/endpoint/common/endpoint_host_services'; import type { IndexedEndpointPolicyResponse } from '../../../../common/endpoint/data_loaders/index_endpoint_policy_response'; import { @@ -47,6 +46,7 @@ import type { IndexEndpointHostsCyTaskOptions, LoadUserAndRoleCyTaskOptions, CreateUserAndRoleCyTaskOptions, + LogItTaskOptions, } from '../types'; import type { DeletedIndexedEndpointRuleAlerts, @@ -60,7 +60,6 @@ import type { IndexedHostsAndAlertsResponse } from '../../../../common/endpoint/ import { deleteIndexedHostsAndAlerts } from '../../../../common/endpoint/index_data'; import type { IndexedCase } from '../../../../common/endpoint/data_loaders/index_case'; import { deleteIndexedCase, indexCase } from '../../../../common/endpoint/data_loaders/index_case'; -import { createRuntimeServices } from '../../../../scripts/endpoint/common/stack_services'; import type { IndexedFleetEndpointPolicyResponse } from '../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy'; import { deleteIndexedFleetEndpointPolicies, @@ -128,18 +127,7 @@ export const dataLoaders = ( ): void => { // Env. variable is set by `cypress_serverless.config.ts` const isServerless = config.env.IS_SERVERLESS; - - const stackServicesPromise = createRuntimeServices({ - kibanaUrl: config.env.KIBANA_URL, - elasticsearchUrl: config.env.ELASTICSEARCH_URL, - fleetServerUrl: config.env.FLEET_SERVER_URL, - username: config.env.KIBANA_USERNAME, - password: config.env.KIBANA_PASSWORD, - esUsername: config.env.ELASTICSEARCH_USERNAME, - esPassword: config.env.ELASTICSEARCH_PASSWORD, - asSuperuser: true, - }); - + const stackServicesPromise = setupStackServicesUsingCypressConfig(config); const roleAndUserLoaderPromise: Promise = stackServicesPromise.then( ({ kbnClient, log }) => { return new TestRoleAndUserLoader(kbnClient, log, isServerless); @@ -147,6 +135,14 @@ export const dataLoaders = ( ); on('task', { + logIt: async ({ level = 'info', data }: LogItTaskOptions): Promise => { + return stackServicesPromise + .then(({ log }) => { + log[level](data); + }) + .then(() => null); + }, + indexFleetEndpointPolicy: async ({ policyName, endpointPackageVersion, @@ -290,42 +286,7 @@ export const dataLoadersForRealEndpoints = ( on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions ): void => { - let fleetServerContainerId: string | undefined; - - const stackServicesPromise = createRuntimeServices({ - kibanaUrl: config.env.KIBANA_URL, - elasticsearchUrl: config.env.ELASTICSEARCH_URL, - fleetServerUrl: config.env.FLEET_SERVER_URL, - username: config.env.KIBANA_USERNAME, - password: config.env.KIBANA_PASSWORD, - esUsername: config.env.ELASTICSEARCH_USERNAME, - esPassword: config.env.ELASTICSEARCH_PASSWORD, - asSuperuser: true, - }); - - on('before:run', async () => { - await startRuntimeServices({ - kibanaUrl: config.env.KIBANA_URL, - elasticUrl: config.env.ELASTICSEARCH_URL, - fleetServerUrl: config.env.FLEET_SERVER_URL, - username: config.env.KIBANA_USERNAME, - password: config.env.KIBANA_PASSWORD, - asSuperuser: true, - }); - const data = await runFleetServerIfNeeded(); - fleetServerContainerId = data?.fleetServerContainerId; - }); - - on('after:run', async () => { - const { log } = await stackServicesPromise; - if (fleetServerContainerId) { - try { - execa.sync('docker', ['kill', fleetServerContainerId]); - } catch (error) { - log.error(error); - } - } - }); + const stackServicesPromise = setupStackServicesUsingCypressConfig(config); on('task', { createEndpointHost: async ( @@ -383,15 +344,7 @@ export const dataLoadersForRealEndpoints = ( path: string; content: string; }): Promise => { - if (process.env.CI) { - await execa('vagrant', ['ssh', '--', `echo ${content} > ${path}`], { - env: { - VAGRANT_CWD, - }, - }); - } else { - await execa(`multipass`, ['exec', hostname, '--', 'sh', '-c', `echo ${content} > ${path}`]); - } + await getHostVmClient(hostname).exec(`echo ${content} > ${path}`); return null; }, @@ -404,16 +357,7 @@ export const dataLoadersForRealEndpoints = ( srcPath: string; destPath: string; }): Promise => { - if (process.env.CI) { - await execa('vagrant', ['upload', srcPath, destPath], { - env: { - VAGRANT_CWD, - }, - }); - } else { - await execa(`multipass`, ['transfer', srcPath, `${hostname}:${destPath}`]); - } - + await getHostVmClient(hostname).transfer(srcPath, destPath); return null; }, @@ -444,38 +388,19 @@ export const dataLoadersForRealEndpoints = ( path: string; password?: string; }): Promise => { - let result; - - if (process.env.CI) { - result = await execa( - `vagrant`, - ['ssh', '--', `unzip -p ${password ? `-P ${password} ` : ''}${path}`], - { - env: { - VAGRANT_CWD, - }, - } - ); - } else { - result = await execa(`multipass`, [ - 'exec', - hostname, - '--', - 'sh', - '-c', - `unzip -p ${password ? `-P ${password} ` : ''}${path}`, - ]); - } - - return result.stdout; + return ( + await getHostVmClient(hostname).exec(`unzip -p ${password ? `-P ${password} ` : ''}${path}`) + ).stdout; }, stopEndpointHost: async (hostName) => { - return stopEndpointHost(hostName); + await stopEndpointHost(hostName); + return null; }, startEndpointHost: async (hostName) => { - return startEndpointHost(hostName); + await startEndpointHost(hostName); + return null; }, }); }; 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 29173c03c3c7..38abf64ce202 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 @@ -64,7 +64,7 @@ Cypress.Commands.addQuery<'findByTestSubj'>( Cypress.Commands.add( 'waitUntil', { prevSubject: 'optional' }, - (subject, fn, { interval = 500, timeout = 30000 } = {}) => { + (subject, fn, { interval = 500, timeout = 30000 } = {}, msg = 'waitUntil()') => { let attempts = Math.floor(timeout / interval); const completeOrRetry = (result: boolean) => { @@ -72,7 +72,7 @@ Cypress.Commands.add( return result; } if (attempts < 1) { - throw new Error(`Timed out while retrying, last result was: {${result}}`); + throw new Error(`${msg}: Timed out while retrying - last result was: [${result}]`); } cy.wait(interval, { log: false }).then(() => { attempts--; @@ -90,7 +90,7 @@ Cypress.Commands.add( return result.then(completeOrRetry); } else { throw new Error( - `Unknown return type from callback: ${Object.prototype.toString.call(result)}` + `${msg}: Unknown return type from callback: ${Object.prototype.toString.call(result)}` ); } }; diff --git a/x-pack/plugins/security_solution/public/management/cypress/support/response_actions.ts b/x-pack/plugins/security_solution/public/management/cypress/support/response_actions.ts index baaf0e12c10e..d0d9befddd6a 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/support/response_actions.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/support/response_actions.ts @@ -9,25 +9,18 @@ import { get } from 'lodash'; +import { setupStackServicesUsingCypressConfig } from './common'; import { getLatestActionDoc, updateActionDoc, waitForNewActionDoc, } from '../../../../scripts/endpoint/common/response_actions'; -import { createRuntimeServices } from '../../../../scripts/endpoint/common/stack_services'; export const responseActionTasks = ( on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions ): void => { - const stackServicesPromise = createRuntimeServices({ - kibanaUrl: config.env.KIBANA_URL, - elasticsearchUrl: config.env.ELASTICSEARCH_URL, - fleetServerUrl: config.env.FLEET_SERVER_URL, - username: config.env.KIBANA_USERNAME, - password: config.env.KIBANA_PASSWORD, - asSuperuser: true, - }); + const stackServicesPromise = setupStackServicesUsingCypressConfig(config); on('task', { getLatestActionDoc: async () => { diff --git a/x-pack/plugins/security_solution/public/management/cypress/support/setup_tooling_log_level.ts b/x-pack/plugins/security_solution/public/management/cypress/support/setup_tooling_log_level.ts index c4c1acd42835..b4901bef9321 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/support/setup_tooling_log_level.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/support/setup_tooling_log_level.ts @@ -13,12 +13,13 @@ import { createToolingLogger } from '../../../../common/endpoint/data_loaders/ut * @param config */ export const setupToolingLogLevel = (config: Cypress.PluginConfigOptions) => { + const log = createToolingLogger(); const defaultToolingLogLevel = config.env.TOOLING_LOG_LEVEL; + log.info(`Cypress config 'env.TOOLING_LOG_LEVEL': ${defaultToolingLogLevel}`); + if (defaultToolingLogLevel && defaultToolingLogLevel !== createToolingLogger.defaultLogLevel) { createToolingLogger.defaultLogLevel = defaultToolingLogLevel; - createToolingLogger().info( - `Default log level for 'createToolingLogger()' set to ${defaultToolingLogLevel}` - ); + log.info(`Default log level for 'createToolingLogger()' set to ${defaultToolingLogLevel}`); } }; diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/fleet.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/fleet.ts index bc0f94d71205..aa2568268f02 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/fleet.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/fleet.ts @@ -10,6 +10,7 @@ import type { GetAgentsResponse, GetInfoResponse, GetPackagePoliciesResponse, + GetOneAgentPolicyResponse, } from '@kbn/fleet-plugin/common'; import { agentRouteService, @@ -26,6 +27,7 @@ import type { import { uninstallTokensRouteService } from '@kbn/fleet-plugin/common/services/routes'; import type { GetUninstallTokensMetadataResponse } from '@kbn/fleet-plugin/common/types/rest_spec/uninstall_token'; import type { UninstallToken } from '@kbn/fleet-plugin/common/types/models/uninstall_token'; +import { logger } from './logger'; import type { IndexedFleetEndpointPolicyResponse } from '../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy'; import { request } from './common'; @@ -138,21 +140,33 @@ export const unenrollAgent = (agentId: string): Cypress.Chainable => { }); }; -export const changeAgentPolicy = ( +export const fetchFleetAgentPolicy = ( + agentPolicyId: string +): Cypress.Chainable => { + return request({ + method: 'GET', + url: agentPolicyRouteService.getInfoPath(agentPolicyId), + }).then((res) => res.body.item); +}; + +export const reAssignFleetAgentToPolicy = ( agentId: string, - policyId: string, - policyRevision: number + policyId: string ): Cypress.Chainable => { - return request({ - method: 'POST', - url: agentRouteService.getReassignPath(agentId), - body: { - policy_id: policyId, - }, - headers: { 'Elastic-Api-Version': API_VERSIONS.public.v1 }, - }).then(() => { - return waitForHasAgentPolicyChanged(agentId, policyId, policyRevision); - }); + return fetchFleetAgentPolicy(policyId) + .then((agentPolicy) => { + return request({ + method: 'POST', + url: agentRouteService.getReassignPath(agentId), + body: { + policy_id: policyId, + }, + headers: { 'Elastic-Api-Version': API_VERSIONS.public.v1 }, + }).then(() => agentPolicy); + }) + .then((agentPolicy) => { + return waitForHasAgentPolicyChanged(agentId, policyId, agentPolicy.revision); + }); }; // only used in "real" endpoint tests not in mocked ones @@ -205,9 +219,11 @@ const waitForIsAgentUnenrolled = (agentId: string): Cypress.Chainable = const waitForHasAgentPolicyChanged = ( agentId: string, policyId: string, + /** The minimum revision number that the agent must report before it is considered "changed" */ policyRevision: number ): Cypress.Chainable => { let isPolicyUpdated = false; + return cy .waitUntil( () => { @@ -218,19 +234,24 @@ const waitForHasAgentPolicyChanged = ( 'elastic-api-version': API_VERSIONS.public.v1, }, }).then((response) => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { status, policy_revision, policy_id } = response.body.item; + + logger.debug('Checking policy data:', { status, policy_revision, policy_id }); + if ( - response.body.item.status !== 'updating' && - response.body.item?.policy_revision === policyRevision && - response.body.item?.policy_id === policyId + status !== 'updating' && + (policy_revision ?? 0) >= policyRevision && + policy_id === policyId ) { isPolicyUpdated = true; - return true; } - return false; + return cy.wrap(isPolicyUpdated); }); }, - { timeout: 120000 } + { timeout: 120000 }, + `Wait for Fleet Agent to report policy id [${policyId}] with revision [${policyRevision}]` ) .then(() => { return isPolicyUpdated; diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/logger.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/logger.ts new file mode 100644 index 000000000000..053ee123c595 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/logger.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. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +export const logger = Object.freeze({ + info: (...data: any): Cypress.Chainable => { + return cy.task('logIt', { level: 'info', data }); + }, + debug: (...data: any): Cypress.Chainable => { + return cy.task('logIt', { level: 'info', data }); + }, + verbose: (...data: any): Cypress.Chainable => { + return cy.task('logIt', { level: 'info', data }); + }, +}); diff --git a/x-pack/plugins/security_solution/public/management/cypress/types.ts b/x-pack/plugins/security_solution/public/management/cypress/types.ts index 6c5dae16100d..8beb150a64d5 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/types.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/types.ts @@ -8,6 +8,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { Role } from '@kbn/security-plugin/common'; +import type { ToolingLog } from '@kbn/tooling-log'; import type { ActionDetails } from '../../../common/endpoint/types'; import type { CyLoadEndpointDataOptions } from './support/plugin_handlers/endpoint_data_loader'; import type { SecurityTestUser } from './common/constants'; @@ -75,3 +76,8 @@ export interface UninstallAgentFromHostTaskOptions { export interface IsAgentAndEndpointUninstalledFromHostTaskOptions { hostname: string; } + +export interface LogItTaskOptions { + level: keyof Pick; + data: any; +} 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 6f392006df8b..34f473a85446 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 @@ -10,6 +10,7 @@ import { join } from 'path'; import fs from 'fs'; import nodeFetch from 'node-fetch'; import { finished } from 'stream/promises'; +import { createToolingLogger } from '../../../common/endpoint/data_loaders/utils'; import { SettingsStorage } from './settings_storage'; export interface DownloadedAgentInfo { @@ -40,6 +41,7 @@ class AgentDownloadStorage extends SettingsStorage private downloadsFolderExists = false; private readonly downloadsDirName = 'agent_download_storage'; private readonly downloadsDirFullPath: string; + private readonly log = createToolingLogger(); constructor() { super('agent_download_storage_settings.json', { @@ -57,6 +59,7 @@ class AgentDownloadStorage extends SettingsStorage if (!this.downloadsFolderExists) { await mkdir(this.downloadsDirFullPath, { recursive: true }); + this.log.debug(`Created directory [this.downloadsDirFullPath] for cached agent downloads`); this.downloadsFolderExists = true; } } @@ -74,6 +77,8 @@ class AgentDownloadStorage extends SettingsStorage } public async downloadAndStore(agentDownloadUrl: string): Promise { + this.log.debug(`Downloading and storing: ${agentDownloadUrl}`); + // TODO: should we add "retry" attempts to file downloads? await this.ensureExists(); @@ -82,6 +87,7 @@ class AgentDownloadStorage extends SettingsStorage // If download is already present on disk, then just return that info. No need to re-download it if (fs.existsSync(newDownloadInfo.fullFilePath)) { + this.log.debug(`Download already cached at [${newDownloadInfo.fullFilePath}]`); return newDownloadInfo; } @@ -104,10 +110,14 @@ class AgentDownloadStorage extends SettingsStorage throw e; } + await this.cleanupDownloads(); + return newDownloadInfo; } public async cleanupDownloads(): Promise<{ deleted: string[] }> { + this.log.debug(`Performing cleanup of cached Agent downlaods`); + const settings = await this.get(); const maxAgeDate = new Date(); const response: { deleted: string[] } = { deleted: [] }; @@ -139,6 +149,9 @@ class AgentDownloadStorage extends SettingsStorage await Promise.allSettled(deleteFilePromises); + this.log.debug(`Deleted [${response.deleted.length}] file(s)`); + this.log.verbose(`files deleted:\n`, response.deleted.join('\n')); + return response; } } 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 303ae81a9197..cb2718e72ba2 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 @@ -8,22 +8,15 @@ import { kibanaPackageJson } from '@kbn/repo-info'; import type { KbnClient } from '@kbn/test'; import type { ToolingLog } from '@kbn/tooling-log'; -import execa from 'execa'; -import assert from 'assert'; -import type { DownloadedAgentInfo } from './agent_downloads_service'; -import { cleanupDownloads, downloadAndStoreAgent } from './agent_downloads_service'; -import { - fetchAgentPolicyEnrollmentKey, - fetchFleetServerUrl, - getAgentDownloadUrl, - unEnrollFleetAgent, - waitForHostToEnroll, -} from './fleet_services'; - -export const VAGRANT_CWD = `${__dirname}/../endpoint_agent_runner/`; +import { prefixedOutputLogger } from './utils'; +import type { HostVm } from './types'; +import type { BaseVmCreateOptions } from './vm_services'; +import { createVm, getHostVmClient } from './vm_services'; +import { downloadAndStoreAgent } from './agent_downloads_service'; +import { enrollHostVmWithFleet, getAgentDownloadUrl, unEnrollFleetAgent } from './fleet_services'; export interface CreateAndEnrollEndpointHostOptions - extends Pick { + extends Pick { kbnClient: KbnClient; log: ToolingLog; /** The fleet Agent Policy ID to use for enrolling the agent */ @@ -41,6 +34,7 @@ export interface CreateAndEnrollEndpointHostOptions export interface CreateAndEnrollEndpointHostResponse { hostname: string; agentId: string; + hostVm: HostVm; } /** @@ -48,7 +42,7 @@ export interface CreateAndEnrollEndpointHostResponse { */ export const createAndEnrollEndpointHost = async ({ kbnClient, - log, + log: _log, agentPolicyId, cpus, disk, @@ -58,85 +52,47 @@ export const createAndEnrollEndpointHost = async ({ useClosestVersionMatch = false, useCache = true, }: CreateAndEnrollEndpointHostOptions): Promise => { - let cacheCleanupPromise: ReturnType = Promise.resolve({ - deleted: [], - }); - + const log = prefixedOutputLogger('createAndEnrollEndpointHost()', _log); + const isRunningInCI = Boolean(process.env.CI); const vmName = hostname ?? `test-host-${Math.random().toString().substring(2, 6)}`; - - const agentDownload = await getAgentDownloadUrl(version, useClosestVersionMatch, log).then<{ - url: string; - cache?: DownloadedAgentInfo; - }>(({ url }) => { - if (useCache) { - cacheCleanupPromise = cleanupDownloads(); - - return downloadAndStoreAgent(url).then((cache) => { - return { - url, - cache, - }; + const { url: agentUrl } = await getAgentDownloadUrl(version, useClosestVersionMatch, log); + const agentDownload = isRunningInCI ? await downloadAndStoreAgent(agentUrl) : undefined; + + // TODO: remove dependency on env. var and keep function pure + const hostVm = process.env.CI + ? await createVm({ + type: 'vagrant', + name: vmName, + log, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + agentDownload: agentDownload!, + disk, + cpus, + memory, + }) + : await createVm({ + type: 'multipass', + log, + name: vmName, + disk, + cpus, + memory, }); - } - - return { url }; - }); - - const [vm, fleetServerUrl, enrollmentToken] = await Promise.all([ - process.env.CI - ? createVagrantVm({ - vmName, - log, - cachedAgentDownload: agentDownload.cache as DownloadedAgentInfo, - }) - : createMultipassVm({ - vmName, - disk, - cpus, - memory, - }), - - fetchFleetServerUrl(kbnClient), - fetchAgentPolicyEnrollmentKey(kbnClient, agentPolicyId), - ]); - - if (!process.env.CI) { - log.verbose(await execa('multipass', ['info', vm.vmName])); - } - - // Some validations before we proceed - assert(agentDownload.url, 'Missing agent download URL'); - assert(fleetServerUrl, 'Fleet server URL not set'); - assert(enrollmentToken, `No enrollment token for agent policy id [${agentPolicyId}]`); - - log.verbose(`Enrolling host [${vm.vmName}] - with fleet-server [${fleetServerUrl}] - using enrollment token [${enrollmentToken}]`); - - const { agentId } = await enrollHostWithFleet({ + const { id: agentId } = await enrollHostVmWithFleet({ kbnClient, log, - fleetServerUrl, - agentDownloadUrl: agentDownload.url, - cachedAgentDownload: agentDownload.cache, - enrollmentToken, - vmName: vm.vmName, - }); - - await cacheCleanupPromise.then((results) => { - if (results.deleted.length > 0) { - log.verbose(`Agent Downloads cache directory was cleaned up and the following ${ - results.deleted.length - } were deleted: -${results.deleted.join('\n')} -`); - } + hostVm, + agentPolicyId, + version, + closestVersionMatch: useClosestVersionMatch, + useAgentCache: useCache, }); return { - hostname: vm.vmName, + hostname: hostVm.name, agentId, + hostVm, }; }; @@ -147,7 +103,7 @@ ${results.deleted.join('\n')} */ export const destroyEndpointHost = async ( kbnClient: KbnClient, - createdHost: CreateAndEnrollEndpointHostResponse + createdHost: Pick ): Promise => { await Promise.all([ deleteMultipassVm(createdHost.hostname), @@ -155,226 +111,14 @@ export const destroyEndpointHost = async ( ]); }; -interface CreateVmResponse { - vmName: string; -} - -interface CreateVagrantVmOptions { - vmName: string; - cachedAgentDownload: DownloadedAgentInfo; - log: ToolingLog; -} - -/** - * Creates a new VM using `vagrant` - */ -const createVagrantVm = async ({ - vmName, - cachedAgentDownload, - log, -}: CreateVagrantVmOptions): Promise => { - try { - await execa.command(`vagrant destroy -f`, { - env: { - VAGRANT_CWD, - }, - // Only `pipe` STDERR to parent process - stdio: ['inherit', 'inherit', 'pipe'], - }); - // eslint-disable-next-line no-empty - } catch (e) {} - - try { - await execa.command(`vagrant up`, { - env: { - VAGRANT_DISABLE_VBOXSYMLINKCREATE: '1', - VAGRANT_CWD, - VMNAME: vmName, - CACHED_AGENT_SOURCE: cachedAgentDownload.fullFilePath, - CACHED_AGENT_FILENAME: cachedAgentDownload.filename, - }, - // Only `pipe` STDERR to parent process - stdio: ['inherit', 'inherit', 'pipe'], - }); - } catch (e) { - log.error(e); - throw e; - } - - return { - vmName, - }; -}; - -interface CreateMultipassVmOptions { - vmName: string; - /** Number of CPUs */ - cpus?: number; - /** Disk size */ - disk?: string; - /** Amount of memory */ - memory?: 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, - }; -}; - export const deleteMultipassVm = async (vmName: string): Promise => { - if (process.env.CI) { - await execa.command(`vagrant destroy -f`, { - env: { - VAGRANT_CWD, - }, - }); - } else { - await execa.command(`multipass delete -p ${vmName}`); - } + await getHostVmClient(vmName).destroy(); }; -interface EnrollHostWithFleetOptions { - kbnClient: KbnClient; - log: ToolingLog; - vmName: string; - agentDownloadUrl: string; - cachedAgentDownload?: DownloadedAgentInfo; - fleetServerUrl: string; - enrollmentToken: string; -} - -const enrollHostWithFleet = async ({ - kbnClient, - log, - vmName, - fleetServerUrl, - agentDownloadUrl, - cachedAgentDownload, - enrollmentToken, -}: EnrollHostWithFleetOptions): Promise<{ agentId: string }> => { - const agentDownloadedFile = agentDownloadUrl.substring(agentDownloadUrl.lastIndexOf('/') + 1); - const vmDirName = agentDownloadedFile.replace(/\.tar\.gz$/, ''); - - if (cachedAgentDownload) { - log.verbose( - `Installing agent on host using cached download from [${cachedAgentDownload.fullFilePath}]` - ); - - if (!process.env.CI) { - // mount local folder on VM - await execa.command( - `multipass mount ${cachedAgentDownload.directory} ${vmName}:~/_agent_downloads` - ); - await execa.command( - `multipass exec ${vmName} -- tar -zxf _agent_downloads/${cachedAgentDownload.filename}` - ); - await execa.command(`multipass unmount ${vmName}:~/_agent_downloads`); - } - } else { - log.verbose(`downloading and installing agent from URL [${agentDownloadUrl}]`); - - if (!process.env.CI) { - // download into VM - 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 = [ - 'sudo', - - './elastic-agent', - - 'install', - - '--insecure', - - '--force', - - '--url', - fleetServerUrl, - - '--enrollment-token', - enrollmentToken, - ]; - - log.info(`Enrolling elastic agent with Fleet`); - if (process.env.CI) { - log.verbose(`Command: vagrant ${agentInstallArguments.join(' ')}`); - - await execa(`vagrant`, ['ssh', '--', `cd ${vmDirName} && ${agentInstallArguments.join(' ')}`], { - env: { - VAGRANT_CWD, - }, - // Only `pipe` STDERR to parent process - stdio: ['inherit', 'inherit', 'pipe'], - }); - } else { - log.verbose(`Command: multipass ${agentInstallArguments.join(' ')}`); - - await execa(`multipass`, [ - 'exec', - vmName, - '--working-directory', - `/home/ubuntu/${vmDirName}`, - - '--', - ...agentInstallArguments, - ]); - } - log.info(`Waiting for Agent to check-in with Fleet`); - - const agent = await waitForHostToEnroll(kbnClient, vmName, 8 * 60 * 1000); - - log.info(`Agent enrolled with Fleet, status: `, agent.status); - - return { - agentId: agent.id, - }; -}; - -export async function getEndpointHosts(): Promise< - Array<{ name: string; state: string; ipv4: string; image: string }> -> { - const output = await execa('multipass', ['list', '--format', 'json']); - return JSON.parse(output.stdout).list; -} - -export function stopEndpointHost(hostName: string) { - if (process.env.CI) { - return execa('vagrant', ['suspend'], { - env: { - VAGRANT_CWD, - VMNAME: hostName, - }, - }); - } - return execa('multipass', ['stop', hostName]); +export async function stopEndpointHost(hostName: string): Promise { + await getHostVmClient(hostName).stop(); } -export function startEndpointHost(hostName: string) { - if (process.env.CI) { - return execa('vagrant', ['up'], { - env: { - VAGRANT_CWD, - }, - }); - } - return execa('multipass', ['start', hostName]); +export async function startEndpointHost(hostName: string): Promise { + await getHostVmClient(hostName).start(); } diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_server/fleet_server_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_server/fleet_server_services.ts index 50384b3541f5..f6590e5ef9e2 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_server/fleet_server_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_server/fleet_server_services.ts @@ -42,6 +42,7 @@ import { import { maybeCreateDockerNetwork, SERVERLESS_NODES, verifyDockerInstalled } from '@kbn/es'; import { resolve } from 'path'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { captureCallingStack, prefixedOutputLogger } from '../utils'; import { createToolingLogger, RETRYABLE_TRANSIENT_ERRORS, @@ -51,6 +52,7 @@ import { isServerlessKibanaFlavor } from '../stack_services'; import type { FormattedAxiosError } from '../format_axios_error'; import { catchAxiosErrorFormatAndThrow } from '../format_axios_error'; import { + ensureFleetSetup, fetchFleetOutputs, fetchFleetServerHostList, fetchFleetServerUrl, @@ -77,6 +79,8 @@ interface StartedServer { url: string; /** Stop server */ stop: () => Promise; + /** Stop server synchronously. Sometimes useful when called from nodeJS unexpected exits. */ + stopNow: () => void; /** Any information about the server */ info?: string; } @@ -94,24 +98,34 @@ interface StartFleetServerOptions { port?: number; } -interface StartedFleetServer extends StartedServer { - /** The policy id that the fleet-server agent is running with */ +export interface StartedFleetServer extends StartedServer { + /** The Fleet Agent policy id that the fleet-server agent is running with */ policyId: string; } +/** + * Starts Fleet Server and connectors it to the stack + * @param kbnClient + * @param logger + * @param policy + * @param version + * @param force + * @param port + */ export const startFleetServer = async ({ kbnClient, - logger, + logger: _logger, policy, version, force = false, port = 8220, }: StartFleetServerOptions): Promise => { + const logger = prefixedOutputLogger('startFleetServer()', _logger); + logger.info(`Starting Fleet Server and connecting it to Kibana`); + logger.debug(captureCallingStack()); return logger.indent(4, async () => { - const isServerless = await isServerlessKibanaFlavor(kbnClient); - // Check if fleet already running if `force` is false if (!force && (await isFleetServerRunning(kbnClient))) { throw new Error( @@ -119,6 +133,9 @@ export const startFleetServer = async ({ ); } + await ensureFleetSetup(kbnClient, logger); + + const isServerless = await isServerlessKibanaFlavor(kbnClient); const policyId = policy || !isServerless ? await getOrCreateFleetServerAgentPolicyId(kbnClient, logger) : ''; const serviceToken = isServerless ? '' : await generateFleetServiceToken(kbnClient, logger); @@ -138,6 +155,18 @@ export const startFleetServer = async ({ }); }; +/** + * Checks if fleet server is already running and if not, then it will attempt to start + * one and connect it to the stack + */ +export const startFleetServerIfNecessary = async ( + options: StartFleetServerOptions +): Promise => { + if (options.force || !(await isFleetServerRunning(options.kbnClient, options.logger))) { + return startFleetServer(options); + } +}; + const getOrCreateFleetServerAgentPolicyId = async ( kbnClient: KbnClient, log: ToolingLog @@ -216,13 +245,15 @@ const startFleetServerWithDocker = async ({ await verifyDockerInstalled(log); let agentVersion = version || (await getAgentVersionMatchingCurrentStack(kbnClient)); + const localhostRealIp = getLocalhostRealIp(); + const fleetServerUrl = `https://${localhostRealIp}:${port}`; - log.info(`Starting a new fleet server using Docker (version: ${agentVersion})`); + log.info( + `Starting a new fleet server using Docker\n Agent version: ${agentVersion}\n Server URL: ${fleetServerUrl}` + ); const response: StartedServer = await log.indent(4, async () => { const isServerless = await isServerlessKibanaFlavor(kbnClient); - const localhostRealIp = getLocalhostRealIp(); - const fleetServerUrl = `https://${localhostRealIp}:${port}`; const esURL = new URL(await getFleetElasticsearchOutputHost(kbnClient)); const containerName = `dev-fleet-server.${port}`; const hostname = `dev-fleet-server.${port}.${Math.random().toString(32).substring(2, 6)}`; @@ -239,12 +270,14 @@ const startFleetServerWithDocker = async ({ - version adjusted to [latest] from [${agentVersion}]`); agentVersion = 'latest'; - await maybeCreateDockerNetwork(log); } else { assert.ok(!!policyId, '`policyId` is required'); assert.ok(!!serviceToken, '`serviceToken` is required'); } + // Create the `elastic` network to use with all containers + await maybeCreateDockerNetwork(log); + try { const dockerArgs = isServerless ? getFleetServerStandAloneDockerArgs({ @@ -266,7 +299,7 @@ const startFleetServerWithDocker = async ({ await execa('docker', ['kill', containerName]) .then(() => { - log.verbose( + log.info( `Killed an existing container with name [${containerName}]. New one will be started.` ); }) @@ -294,25 +327,27 @@ const startFleetServerWithDocker = async ({ await waitForFleetServerToRegisterWithElasticsearch(kbnClient, hostname, 120000); } else { - log.info('Waiting for server to show up in Kibana Fleet'); - - const fleetServerAgent = await waitForHostToEnroll(kbnClient, hostname, 120000); - - log.verbose(`Fleet server enrolled agent:\n${JSON.stringify(fleetServerAgent, null, 2)}`); + await waitForHostToEnroll(kbnClient, log, hostname, 120000); } - fleetServerVersionInfo = ( - await execa('docker', [ - 'exec', - containerName, - '/bin/bash', - '-c', - './elastic-agent version', - ]).catch((err) => { - log.verbose(`Failed to retrieve agent version information from running instance.`, err); - return { stdout: 'Unable to retrieve version information' }; - }) - ).stdout; + fleetServerVersionInfo = isServerless + ? // `/usr/bin/fleet-server` process does not seem to support a `--version` type of argument + 'Running latest standalone fleet server' + : ( + await execa('docker', [ + 'exec', + containerName, + '/bin/bash', + '-c', + '/usr/share/elastic-agent/elastic-agent version', + ]).catch((err) => { + log.verbose( + `Failed to retrieve agent version information from running instance.`, + err + ); + return { stdout: 'Unable to retrieve version information' }; + }) + ).stdout; } catch (error) { log.error(dump(error)); throw error; @@ -335,8 +370,17 @@ Kill container: ${chalk.cyan(`docker kill ${containerId}`)} url: fleetServerUrl, info, stop: async () => { + log.info( + `Stopping (kill) fleet server. Container name [${containerName}] id [${containerId}]` + ); await execa('docker', ['kill', containerId]); }, + stopNow: () => { + log.info( + `Stopping (kill) fleet server. Container name [${containerName}] id [${containerId}]` + ); + execa.sync('docker', ['kill', containerId]); + }, }; }); @@ -371,6 +415,9 @@ const getFleetServerManagedDockerArgs = ({ '--restart', 'no', + '--net', + 'elastic', + '--add-host', 'host.docker.internal:host-gateway', @@ -494,7 +541,7 @@ const addFleetServerHostToFleetSettings = async ( const newFleetHostEntry: PostFleetServerHostsRequest['body'] = { name: `Dev fleet server running on localhost`, host_urls: [fleetServerHostUrl], - is_default: !exitingFleetServerHostList.total, + is_default: true, }; const { item } = await kbnClient @@ -630,12 +677,13 @@ export const isFleetServerRunning = async ( httpsAgent: new https.Agent({ rejectUnauthorized: false }), }) .then((response) => { - log.verbose(`Fleet server is up and running as [${fleetServerUrl}]`, response.data); + log.debug(`Fleet server is up and running at [${fleetServerUrl}]. Status: `, response.data); return true; }) .catch(catchAxiosErrorFormatAndThrow) .catch((e) => { - log.verbose(`Fleet server not up. Attempt to call [${url.toString()}] failed with:`, e); + log.debug(`Fleet server not up at [${fleetServerUrl}]`); + log.verbose(`Call to [${url.toString()}] failed with:`, e); return false; }); }; 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 80825c84fb19..26ca9d647439 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 @@ -5,7 +5,7 @@ * 2.0. */ -import { map, pick } from 'lodash'; +import { map, memoize, pick } from 'lodash'; import type { Client, estypes } from '@elastic/elasticsearch'; import type { Agent, @@ -23,6 +23,7 @@ import type { PackagePolicy, GetInfoResponse, GetOneAgentPolicyResponse, + PostFleetSetupResponse, } from '@kbn/fleet-plugin/common'; import { AGENT_API_ROUTES, @@ -35,6 +36,7 @@ import { APP_API_ROUTES, epmRouteService, PACKAGE_POLICY_API_ROUTES, + SETUP_API_ROUTE, } from '@kbn/fleet-plugin/common'; import type { ToolingLog } from '@kbn/tooling-log'; import type { KbnClient } from '@kbn/test'; @@ -56,6 +58,7 @@ import nodeFetch from 'node-fetch'; import semver from 'semver'; import axios from 'axios'; import { userInfo } from 'os'; +import { isFleetServerRunning } from './fleet_server/fleet_server_services'; import { getEndpointPackageInfo } from '../../../common/endpoint/utils/package'; import type { DownloadAndStoreAgentResponse } from './agent_downloads_service'; import { downloadAndStoreAgent } from './agent_downloads_service'; @@ -72,6 +75,9 @@ import { FleetAgentGenerator } from '../../../common/endpoint/data_generators/fl const fleetGenerator = new FleetAgentGenerator(); const CURRENT_USERNAME = userInfo().username.toLowerCase(); +const DEFAULT_AGENT_POLICY_NAME = `${CURRENT_USERNAME} test policy`; +/** A Fleet agent policy that includes integrations that don't actually require an agent to run on a host. Example: SenttinelOne */ +export const DEFAULT_AGENTLESS_INTEGRATIONS_AGENT_POLICY_NAME = `${CURRENT_USERNAME} - agentless integrations`; export const checkInFleetAgent = async ( esClient: Client, @@ -151,14 +157,18 @@ export const fetchFleetAgents = async ( * Will keep querying Fleet list of agents until the given `hostname` shows up as healthy * * @param kbnClient + * @param log * @param hostname * @param timeoutMs */ export const waitForHostToEnroll = async ( kbnClient: KbnClient, + log: ToolingLog, hostname: string, timeoutMs: number = 30000 ): Promise => { + log.info(`Waiting for host [${hostname}] to enroll with fleet`); + const started = new Date(); const hasTimedOut = (): boolean => { const elapsedTime = Date.now() - started.getTime(); @@ -190,14 +200,17 @@ export const waitForHostToEnroll = async ( if (!found) { throw Object.assign( new Error( - `Timed out waiting for host [${hostname}] to show up in Fleet in ${ - timeoutMs / 60 / 1000 + `Timed out waiting for host [${hostname}] to show up in Fleet. Waited ${ + timeoutMs / 1000 } seconds` ), { agentId, hostname } ); } + log.debug(`Host [${hostname}] has been enrolled with fleet`); + log.verbose(found); + return found; }; @@ -544,7 +557,10 @@ export const fetchFleetOutputs = async (kbnClient: KbnClient): Promise => { +export const getFleetElasticsearchOutputHost = async ( + kbnClient: KbnClient, + log: ToolingLog = createToolingLogger() +): Promise => { const outputs = await fetchFleetOutputs(kbnClient); let host: string = ''; @@ -555,6 +571,7 @@ export const getFleetElasticsearchOutputHost = async (kbnClient: KbnClient): Pro } if (!host) { + log.error(`Outputs returned from Fleet:\n${JSON.stringify(outputs, null, 2)}`); throw new Error(`An output for Elasticsearch was not found in Fleet settings`); } @@ -578,7 +595,10 @@ interface EnrollHostVmWithFleetOptions { } /** - * Installs the Elastic agent on the provided Host VM and enrolls with it Fleet + * Installs the Elastic agent on the provided Host VM and enrolls with it Fleet. + * + * NOTE: this method assumes that FLeet-Server is already setup and running. + * * @param hostVm * @param kbnClient * @param log @@ -600,6 +620,10 @@ export const enrollHostVmWithFleet = async ({ }: EnrollHostVmWithFleetOptions): Promise => { log.info(`Enrolling host VM [${hostVm.name}] with Fleet`); + if (!(await isFleetServerRunning(kbnClient))) { + throw new Error(`Fleet server does not seem to be running on this instance of kibana!`); + } + const agentVersion = version || (await getAgentVersionMatchingCurrentStack(kbnClient)); const agentUrlInfo = await getAgentDownloadUrl(agentVersion, closestVersionMatch, log); @@ -609,26 +633,30 @@ export const enrollHostVmWithFleet = async ({ log.info(`Installing Elastic Agent`); - // Mount the directory where the agent download cache is located - if (useAgentCache) { - const hostVmDownloadsDir = '/home/ubuntu/_agent_downloads'; + // For multipass, we need to place the Agent archive in the VM - either mounting local cache + // directory or downloading it directly from inside of the VM. + // For Vagrant, the archive is already in the VM - it was done during VM creation. + if (hostVm.type === 'multipass') { + if (useAgentCache) { + const hostVmDownloadsDir = '/home/ubuntu/_agent_downloads'; - log.debug( - `Mounting agents download cache directory [${agentDownload.directory}] to Host VM at [${hostVmDownloadsDir}]` - ); - const downloadsMount = await hostVm.mount(agentDownload.directory, hostVmDownloadsDir); + log.debug( + `Mounting agents download cache directory [${agentDownload.directory}] to Host VM at [${hostVmDownloadsDir}]` + ); + const downloadsMount = await hostVm.mount(agentDownload.directory, hostVmDownloadsDir); - log.debug(`Extracting download archive on host VM`); - await hostVm.exec(`tar -zxf ${downloadsMount.hostDir}/${agentDownload.filename}`); + log.debug(`Extracting download archive on host VM`); + await hostVm.exec(`tar -zxf ${downloadsMount.hostDir}/${agentDownload.filename}`); - await downloadsMount.unmount(); - } else { - log.debug(`Downloading Elastic Agent to host VM`); - await hostVm.exec(`curl -L ${agentDownload.url} -o ${agentDownload.filename}`); + await downloadsMount.unmount(); + } else { + log.debug(`Downloading Elastic Agent to host VM`); + await hostVm.exec(`curl -L ${agentDownload.url} -o ${agentDownload.filename}`); - log.debug(`Extracting download archive on host VM`); - await hostVm.exec(`tar -zxf ${agentDownload.filename}`); - await hostVm.exec(`rm -f ${agentDownload.filename}`); + log.debug(`Extracting download archive on host VM`); + await hostVm.exec(`tar -zxf ${agentDownload.filename}`); + await hostVm.exec(`rm -f ${agentDownload.filename}`); + } } const policyId = agentPolicyId || (await getOrCreateDefaultAgentPolicy({ kbnClient, log })).id; @@ -640,7 +668,7 @@ export const enrollHostVmWithFleet = async ({ const agentEnrollCommand = [ 'sudo', - `/home/ubuntu/${agentUrlInfo.dirName}/elastic-agent`, + `./${agentUrlInfo.dirName}/elastic-agent`, 'install', @@ -660,15 +688,13 @@ export const enrollHostVmWithFleet = async ({ await hostVm.exec(agentEnrollCommand); - log.info(`Waiting for Agent to check-in with Fleet`); - const agent = await waitForHostToEnroll(kbnClient, hostVm.name, timeoutMs); - - return agent; + return waitForHostToEnroll(kbnClient, log, hostVm.name, timeoutMs); }; interface GetOrCreateDefaultAgentPolicyOptions { kbnClient: KbnClient; log: ToolingLog; + policyName?: string; } /** @@ -676,14 +702,15 @@ interface GetOrCreateDefaultAgentPolicyOptions { * policy already exists, then it will be reused. * @param kbnClient * @param log + * @param policyName */ export const getOrCreateDefaultAgentPolicy = async ({ kbnClient, log, + policyName = DEFAULT_AGENT_POLICY_NAME, }: GetOrCreateDefaultAgentPolicyOptions): Promise => { - const agentPolicyName = `${CURRENT_USERNAME} test policy`; const existingPolicy = await fetchAgentPolicyList(kbnClient, { - kuery: `${AGENT_POLICY_SAVED_OBJECT_TYPE}.name: "${agentPolicyName}"`, + kuery: `${AGENT_POLICY_SAVED_OBJECT_TYPE}.name: "${policyName}"`, }); if (existingPolicy.items[0]) { @@ -696,13 +723,13 @@ export const getOrCreateDefaultAgentPolicy = async ({ log.info(`Creating new default test/dev Fleet agent policy`); const newAgentPolicyData: CreateAgentPolicyRequest['body'] = { - name: agentPolicyName, - description: `Policy created by security solution tooling`, + name: policyName, + description: `Policy created by security solution tooling: ${__filename}`, namespace: 'default', monitoring_enabled: ['logs', 'metrics'], }; - const newAgentPolicy = kbnClient + const newAgentPolicy = await kbnClient .request({ path: AGENT_POLICY_API_ROUTES.CREATE_PATTERN, headers: { @@ -793,7 +820,7 @@ export const addSentinelOneIntegrationToAgentPolicy = async ({ agentPolicyId, consoleUrl, apiToken, - integrationPolicyName = `SentinelOne policy (${Math.random().toString().substring(3)})`, + integrationPolicyName = `SentinelOne policy (${Math.random().toString().substring(2, 6)})`, force = false, }: AddSentinelOneIntegrationToAgentPolicyOptions): Promise => { // If `force` is `false and agent policy already has a SentinelOne integration, exit here @@ -1092,3 +1119,27 @@ export const addEndpointIntegrationToAgentPolicy = async ({ return newIntegrationPolicy; }; + +/** + * Calls the fleet setup API to ensure fleet configured with default settings + * @param kbnClient + * @param log + */ +export const ensureFleetSetup = memoize( + async (kbnClient: KbnClient, log: ToolingLog): Promise => { + const setupResponse = await kbnClient + .request({ + path: SETUP_API_ROUTE, + headers: { 'Elastic-Api-Version': API_VERSIONS.public.v1 }, + method: 'POST', + }) + .catch(catchAxiosErrorFormatAndThrow); + + if (!setupResponse.data.isInitialized) { + log.verbose(`Fleet setup response:`, setupResponse); + throw new Error(`Call to initialize Fleet [${SETUP_API_ROUTE}] failed`); + } + + return setupResponse.data; + } +); diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/types.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/types.ts index 44bdcff32840..38256f1c774b 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/types.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/types.ts @@ -14,8 +14,12 @@ export interface HostVm { exec: (command: string) => Promise; mount: (localDir: string, hostVmDir: string) => Promise; unmount: (hostVmDir: string) => Promise; + /** Uploads/copies a file from the local machine to the VM */ + transfer: (localFilePath: string, destFilePath: string) => Promise; destroy: () => Promise; info: () => string; + stop: () => void; + start: () => void; } export type SupportedVmManager = 'multipass' | 'vagrant'; @@ -28,3 +32,9 @@ export interface HostVmMountResponse { hostDir: string; unmount: () => Promise; } +export interface HostVmTransferResponse { + /** The file path of the file on the host vm */ + filePath: string; + /** Delete the file from the host VM */ + delete: () => Promise; +} diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/utils.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/utils.ts new file mode 100644 index 000000000000..3c8e227c8927 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/utils.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. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import type { ToolingLog } from '@kbn/tooling-log'; +import chalk from 'chalk'; + +/** + * Capture and return the calling stack for the context that called this utility. + */ +export const captureCallingStack = () => { + const s = { stack: '' }; + Error.captureStackTrace(s); + return `Called from:\n${s.stack.split('\n').slice(3).join('\n')}`; +}; + +/** + * Returns a logger that intercepts calls to the ToolingLog instance methods passed in input + * and prefix it with the provided value. Useful in order to track log entries, especially when + * logging output from multiple sources is concurrently being output to the same source + * (ex. CI jobs and output to stdout). + * + * @param prefix + * @param log + * + * @example + * const logger = new ToolingLog(); + * const prefixedLogger = prefixedOutputLogger('my_log', logger); + * + * prefixedLogger.info('log something'); // => info [my_log] log something + */ +export const prefixedOutputLogger = (prefix: string, log: ToolingLog): ToolingLog => { + const styledPrefix = `[${chalk.grey(prefix)}]`; + const logIt = (type: keyof ToolingLog, ...args: any) => { + return log[type](styledPrefix, ...args); + }; + + const logger: Partial = { + info: logIt.bind(null, 'info'), + debug: logIt.bind(null, 'debug'), + verbose: logIt.bind(null, 'verbose'), + success: logIt.bind(null, 'success'), + warning: logIt.bind(null, 'warning'), + write: logIt.bind(null, 'write'), + }; + + const proxy = new Proxy(log, { + get(target: ToolingLog, prop: keyof ToolingLog, receiver: any): any { + if (prop in logger) { + return logger[prop]; + } + + return log[prop]; + }, + }); + + return proxy; +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/Vagrantfile b/x-pack/plugins/security_solution/scripts/endpoint/common/vagrant/Vagrantfile similarity index 76% rename from x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/Vagrantfile rename to x-pack/plugins/security_solution/scripts/endpoint/common/vagrant/Vagrantfile index a6b93e8de47b..fd33efaeab62 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/Vagrantfile +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/vagrant/Vagrantfile @@ -1,3 +1,7 @@ +# ------------------------------------------------------------------------------------ +# Vagrant setup for running Elastic agent on the created VM.0 +# This setup is mostly used for CI runs, since multipass is not used in that env. +# ------------------------------------------------------------------------------------ hostname = ENV["VMNAME"] || 'ubuntu' cachedAgentSource = ENV["CACHED_AGENT_SOURCE"] || '' cachedAgentFilename = ENV["CACHED_AGENT_FILENAME"] || '' 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 ca0a83d2cdd4..a0efd908f80d 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 @@ -8,10 +8,17 @@ import type { ToolingLog } from '@kbn/tooling-log'; import execa from 'execa'; import chalk from 'chalk'; +import { userInfo } from 'os'; +import { join as pathJoin, dirname } from 'path'; +import type { DownloadedAgentInfo } from './agent_downloads_service'; +import { BaseDataGenerator } from '../../../common/endpoint/data_generators/base_data_generator'; import { createToolingLogger } from '../../../common/endpoint/data_loaders/utils'; import type { HostVm, HostVmExecResponse, SupportedVmManager } from './types'; -interface BaseVmCreateOptions { +const baseGenerator = new BaseDataGenerator(); +export const DEFAULT_VAGRANTFILE = pathJoin(__dirname, 'vagrant', 'Vagrantfile'); + +export interface BaseVmCreateOptions { name: string; /** Number of CPUs */ cpus?: number; @@ -21,24 +28,21 @@ interface BaseVmCreateOptions { memory?: string; } -interface CreateVmOptions extends BaseVmCreateOptions { - /** The type of VM manager to use when creating the VM host */ - type: SupportedVmManager; - log?: ToolingLog; -} +type CreateVmOptions = CreateMultipassVmOptions | CreateVagrantVmOptions; /** * Creates a new VM */ -export const createVm = async ({ type, ...options }: CreateVmOptions): Promise => { - if (type === 'multipass') { +export const createVm = async (options: CreateVmOptions): Promise => { + if (options.type === 'multipass') { return createMultipassVm(options); } - throw new Error(`VM type ${type} not yet supported`); + return createVagrantVm(options); }; interface CreateMultipassVmOptions extends BaseVmCreateOptions { + type: SupportedVmManager & 'multipass'; name: string; log?: ToolingLog; } @@ -97,11 +101,13 @@ export const createMultipassHostVmClient = ( }; const unmount = async (hostVmDir: string) => { - await execa.command(`multipass unmount ${name}:${hostVmDir}`); + const response = await execa.command(`multipass unmount ${name}:${hostVmDir}`); + log.verbose(`multipass unmount response:\n`, response); }; const mount = async (localDir: string, hostVmDir: string) => { - await execa.command(`multipass mount ${localDir} ${name}:${hostVmDir}`); + const response = await execa.command(`multipass mount ${localDir} ${name}:${hostVmDir}`); + log.verbose(`multipass mount response:\n`, response); return { hostDir: hostVmDir, @@ -109,6 +115,30 @@ export const createMultipassHostVmClient = ( }; }; + const start = async () => { + const response = await execa.command(`multipass start ${name}`); + log.verbose(`multipass start response:\n`, response); + }; + + const stop = async () => { + const response = await execa.command(`multipass stop ${name}`); + log.verbose(`multipass stop response:\n`, response); + }; + + const transfer: HostVm['transfer'] = async (localFilePath, destFilePath) => { + const response = await execa.command( + `multipass transfer ${localFilePath} ${name}:${destFilePath}` + ); + log.verbose(`Transferred file to VM [${name}]:`, response); + + return { + filePath: destFilePath, + delete: async () => { + return exec(`rm ${destFilePath}`); + }, + }; + }; + return { type: 'multipass', name, @@ -117,5 +147,233 @@ export const createMultipassHostVmClient = ( info, mount, unmount, + transfer, + start, + stop, }; }; + +/** + * Generates a unique Virtual Machine name using the current user's `username` + * @param identifier + */ +export const generateVmName = (identifier: string = baseGenerator.randomUser()): string => { + return `${userInfo().username.toLowerCase().replaceAll('.', '-')}-${identifier + .toLowerCase() + .replace('.', '-')}-${Math.random().toString().substring(2, 6)}`; +}; + +/** + * Checks if the count of VM running under Multipass is greater than the `threshold` passed on + * input and if so, it will return a message indicate so. Useful to remind users of the amount of + * VM currently running. + * @param threshold + */ +export const getMultipassVmCountNotice = async (threshold: number = 1): Promise => { + const response = await execa.command(`multipass list --format=json`); + + const output: { list: Array<{ ipv4: string; name: string; release: string; state: string }> } = + JSON.parse(response.stdout); + + if (output.list.length > threshold) { + return `----------------------------------------------------------------- +${chalk.red('NOTE:')} ${chalk.bold( + chalk.cyan(`You currently have ${chalk.red(output.list.length)} VMs running.`) + )} Remember to delete those + no longer being used. + View running VMs: ${chalk.bold('multipass list')} + ----------------------------------------------------------------- +`; + } + + return ''; +}; + +interface CreateVagrantVmOptions extends BaseVmCreateOptions { + type: SupportedVmManager & 'vagrant'; + + name: string; + /** + * The downloaded agent information. The Agent file will be uploaded to the Vagrant VM and + * made available under the default login home directory (`~/agent-filename`) + */ + agentDownload: DownloadedAgentInfo; + /** + * The path to the Vagrantfile to use to provision the VM. Defaults to Vagrantfile under: + * `x-pack/plugins/security_solution/scripts/endpoint/common/vagrant/Vagrantfile` + */ + vagrantFile?: string; + log?: ToolingLog; +} + +/** + * Creates a new VM using `vagrant` + */ +const createVagrantVm = async ({ + name, + log = createToolingLogger(), + agentDownload: { fullFilePath: agentFullFilePath, filename: agentFileName }, + vagrantFile = DEFAULT_VAGRANTFILE, + memory, + cpus, + disk, +}: CreateVagrantVmOptions): Promise => { + log.debug(`Using Vagrantfile: ${vagrantFile}`); + + const VAGRANT_CWD = dirname(vagrantFile); + + // Destroy the VM running (if any) with the provided vagrant file before re-creating it + try { + await execa.command(`vagrant destroy -f`, { + env: { + VAGRANT_CWD, + }, + // Only `pipe` STDERR to parent process + stdio: ['inherit', 'inherit', 'pipe'], + }); + // eslint-disable-next-line no-empty + } catch (e) {} + + if (memory || cpus || disk) { + log.warning( + `cpu, memory and disk options ignored for creation of vm via Vagrant. These should be defined in the Vagrantfile` + ); + } + + try { + const vagrantUpResponse = ( + await execa.command(`vagrant up`, { + env: { + VAGRANT_DISABLE_VBOXSYMLINKCREATE: '1', + VAGRANT_CWD, + VMNAME: name, + CACHED_AGENT_SOURCE: agentFullFilePath, + CACHED_AGENT_FILENAME: agentFileName, + }, + // Only `pipe` STDERR to parent process + stdio: ['inherit', 'inherit', 'pipe'], + }) + ).stdout; + + log.debug(`Vagrant up command response: `, vagrantUpResponse); + } catch (e) { + log.error(e); + throw e; + } + + return createVagrantHostVmClient(name, undefined, log); +}; + +/** + * Creates a generic interface (`HotVm`) for interacting with a VM created by Vagrant + * @param name + * @param log + * @param vagrantFile + */ +export const createVagrantHostVmClient = ( + name: string, + vagrantFile: string = DEFAULT_VAGRANTFILE, + log: ToolingLog = createToolingLogger() +): HostVm => { + const VAGRANT_CWD = dirname(vagrantFile); + const execaOptions: execa.Options = { + env: { + VAGRANT_CWD, + }, + stdio: ['inherit', 'pipe', 'pipe'], + }; + + log.debug(`Creating Vagrant VM client for [${name}] with vagrantfile [${vagrantFile}]`); + + const exec = async (command: string): Promise => { + const execResponse = await execa.command(`vagrant ssh -- ${command}`, execaOptions); + + log.verbose(execResponse); + + return { + stdout: execResponse.stdout, + stderr: execResponse.stderr, + exitCode: execResponse.exitCode, + }; + }; + + const destroy = async (): Promise => { + const destroyResponse = await execa.command(`vagrant destroy -f`, execaOptions); + + log.debug(`VM [${name}] was destroyed successfully`, destroyResponse); + }; + + const info = () => { + return `VM created using Vagrant. + VM Name: ${name} + + Shell access: ${chalk.cyan(`vagrant ssh ${name}`)} + Delete VM: ${chalk.cyan(`vagrant destroy ${name} -f`)} +`; + }; + + const unmount = async (_: string) => { + throw new Error('VM action `unmount`` not currently supported for vagrant'); + }; + + const mount = async (_: string, __: string) => { + throw new Error('VM action `mount` not currently supported for vagrant'); + }; + + const start = async () => { + const response = await execa.command(`vagrant up`, execaOptions); + log.verbose('vagrant up response:\n', response); + }; + + const stop = async () => { + const response = await execa.command(`vagrant suspend`, execaOptions); + log.verbose('vagrant suspend response:\n', response); + }; + + const transfer: HostVm['transfer'] = async (localFilePath, destFilePath) => { + const response = await execa.command( + `vagrant upload ${localFilePath} ${destFilePath}`, + execaOptions + ); + log.verbose(`Transferred file to VM [${name}]:`, response); + + return { + filePath: destFilePath, + delete: async () => { + return exec(`rm ${destFilePath}`); + }, + }; + }; + + return { + type: 'vagrant', + name, + exec, + destroy, + info, + mount, + unmount, + transfer, + start, + stop, + }; +}; + +/** + * create and return a Host VM client client + * @param hostname + * @param type + * @param vagrantFile + * @param log + */ +export const getHostVmClient = ( + hostname: string, + type: SupportedVmManager = process.env.CI ? 'vagrant' : 'multipass', + /** Will only be used if `type` is `vagrant` */ + vagrantFile: string = DEFAULT_VAGRANTFILE, + log: ToolingLog = createToolingLogger() +): HostVm => { + return type === 'vagrant' + ? createVagrantHostVmClient(hostname, vagrantFile, log) + : createMultipassHostVmClient(hostname, log); +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/elastic_endpoint.ts b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/elastic_endpoint.ts index 0c07013f8c7b..e569580eb735 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/elastic_endpoint.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/elastic_endpoint.ts @@ -5,9 +5,7 @@ * 2.0. */ -import { userInfo } from 'os'; -import execa from 'execa'; -import chalk from 'chalk'; +import { generateVmName } from '../common/vm_services'; import { createAndEnrollEndpointHost } from '../common/endpoint_host_services'; import { addEndpointIntegrationToAgentPolicy, @@ -18,6 +16,7 @@ import { dump } from './utils'; export const enrollEndpointHost = async (): Promise => { let vmName; + const { log, kbnClient, @@ -28,8 +27,6 @@ export const enrollEndpointHost = async (): Promise => { log.indent(4); try { - const uniqueId = Math.random().toString().substring(2, 6); - const username = userInfo().username.toLowerCase().replaceAll('.', '-'); // Multipass doesn't like periods in username const policyId: string = policy || (await getOrCreateAgentPolicyId()); if (!policyId) { @@ -40,11 +37,11 @@ export const enrollEndpointHost = async (): Promise => { throw new Error(`No 'version' specified`); } - vmName = `${username}-dev-${uniqueId}`; + vmName = generateVmName('dev'); log.info(`Creating VM named: ${vmName}`); - await createAndEnrollEndpointHost({ + const { hostVm } = await createAndEnrollEndpointHost({ kbnClient, log, hostname: vmName, @@ -54,23 +51,7 @@ export const enrollEndpointHost = async (): Promise => { disk: '8G', }); - if (process.env.CI) { - log.info(`VM created using Vagrant. - VM Name: ${vmName} - Elastic Agent Version: ${version} - - Shell access: ${chalk.bold(`vagrant ssh ${vmName}`)} - Delete VM: ${chalk.bold(`vagrant destroy ${vmName} -f`)} - `); - } else { - log.info(`VM created using Multipass. - VM Name: ${vmName} - Elastic Agent Version: ${version} - - Shell access: ${chalk.bold(`multipass shell ${vmName}`)} - Delete VM: ${chalk.bold(`multipass delete -p ${vmName}${await getVmCountNotice()}`)} - `); - } + log.info(hostVm.info()); } catch (error) { log.error(dump(error)); log.indent(-4); @@ -90,25 +71,3 @@ const getOrCreateAgentPolicyId = async (): Promise => { return agentPolicy.id; }; - -const getVmCountNotice = async (threshold: number = 1): Promise => { - const response = await execa.command(`multipass list --format=json`); - - const output: { list: Array<{ ipv4: string; name: string; release: string; state: string }> } = - JSON.parse(response.stdout); - - if (output.list.length > threshold) { - return ` - ------------------------------------------------------------------ -${chalk.red('NOTE:')} ${chalk.bold( - `You currently have ${output.list.length} VMs running.` - )} Remember to delete those - no longer being used. - View running VMs: ${chalk.bold('multipass list')} - ----------------------------------------------------------------- -`; - } - - return ''; -}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/fleet_server.ts b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/fleet_server.ts deleted file mode 100644 index ec13f2f6ff1b..000000000000 --- a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/fleet_server.ts +++ /dev/null @@ -1,506 +0,0 @@ -/* - * 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 { - CA_TRUSTED_FINGERPRINT, - FLEET_SERVER_CERT_PATH, - FLEET_SERVER_KEY_PATH, - fleetServerDevServiceAccount, -} from '@kbn/dev-utils'; -import type { - AgentPolicy, - CreateAgentPolicyResponse, - GetPackagePoliciesResponse, - Output, - PackagePolicy, -} from '@kbn/fleet-plugin/common'; -import { - AGENT_POLICY_API_ROUTES, - API_VERSIONS, - APP_API_ROUTES, - FLEET_SERVER_PACKAGE, - PACKAGE_POLICY_API_ROUTES, - PACKAGE_POLICY_SAVED_OBJECT_TYPE, -} from '@kbn/fleet-plugin/common'; -import type { - FleetServerHost, - GenerateServiceTokenResponse, - GetOneOutputResponse, - GetOutputsResponse, - PutOutputRequest, -} from '@kbn/fleet-plugin/common/types'; -import { - fleetServerHostsRoutesService, - outputRoutesService, -} from '@kbn/fleet-plugin/common/services'; -import execa from 'execa'; -import type { - PostFleetServerHostsRequest, - PostFleetServerHostsResponse, -} from '@kbn/fleet-plugin/common/types/rest_spec/fleet_server_hosts'; -import chalk from 'chalk'; -import { maybeCreateDockerNetwork, SERVERLESS_NODES, verifyDockerInstalled } from '@kbn/es'; -import { FLEET_SERVER_CUSTOM_CONFIG } from '../common/fleet_server/fleet_server_services'; -import { isServerlessKibanaFlavor } from '../common/stack_services'; -import type { FormattedAxiosError } from '../common/format_axios_error'; -import { catchAxiosErrorFormatAndThrow } from '../common/format_axios_error'; -import { isLocalhost } from '../common/is_localhost'; -import { dump } from './utils'; -import { fetchFleetServerUrl, waitForHostToEnroll } from '../common/fleet_services'; -import { getRuntimeServices } from './runtime'; - -export const runFleetServerIfNeeded = async (): Promise< - { fleetServerContainerId: string; fleetServerAgentPolicyId: string | undefined } | undefined -> => { - let fleetServerContainerId; - let fleetServerAgentPolicyId; - let serviceToken; - - const { - log, - kibana: { isLocalhost: isKibanaOnLocalhost }, - kbnClient, - } = getRuntimeServices(); - - log.info(`Setting up fleet server (if necessary)`); - log.indent(4); - const isServerless = await isServerlessKibanaFlavor(kbnClient); - - await verifyDockerInstalled(log); - await maybeCreateDockerNetwork(log); - - try { - if (isServerless) { - fleetServerContainerId = await startFleetServerStandAloneWithDocker(); - } else { - fleetServerAgentPolicyId = await getOrCreateFleetServerAgentPolicyId(); - serviceToken = await generateFleetServiceToken(); - if (isKibanaOnLocalhost) { - await configureFleetIfNeeded(); - } - fleetServerContainerId = await startFleetServerWithDocker({ - policyId: fleetServerAgentPolicyId, - serviceToken, - }); - } - } catch (error) { - log.error(dump(error)); - log.indent(-4); - throw error; - } - - log.indent(-4); - - return { fleetServerContainerId, fleetServerAgentPolicyId }; -}; - -const getFleetServerPackagePolicy = async (): Promise => { - const { kbnClient } = getRuntimeServices(); - - return kbnClient - .request({ - method: 'GET', - path: PACKAGE_POLICY_API_ROUTES.LIST_PATTERN, - headers: { - 'elastic-api-version': API_VERSIONS.public.v1, - }, - query: { - perPage: 1, - kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: "${FLEET_SERVER_PACKAGE}"`, - }, - }) - .then((response) => response.data.items[0]); -}; - -const getOrCreateFleetServerAgentPolicyId = async (): Promise => { - const { log, kbnClient } = getRuntimeServices(); - - const existingFleetServerIntegrationPolicy = await getFleetServerPackagePolicy(); - - if (existingFleetServerIntegrationPolicy) { - log.verbose( - `Found existing Fleet Server Policy: ${JSON.stringify( - existingFleetServerIntegrationPolicy, - null, - 2 - )}` - ); - log.info( - `Using existing Fleet Server agent policy id: ${existingFleetServerIntegrationPolicy.policy_id}` - ); - - return existingFleetServerIntegrationPolicy.policy_id; - } - - log.info(`Creating new Fleet Server policy`); - - const createdFleetServerPolicy: AgentPolicy = await kbnClient - .request({ - method: 'POST', - path: AGENT_POLICY_API_ROUTES.CREATE_PATTERN, - headers: { - 'elastic-api-version': API_VERSIONS.public.v1, - }, - body: { - name: `Fleet Server policy (${Math.random().toString(32).substring(2)})`, - description: `Created by CLI Tool via: ${__filename}`, - namespace: 'default', - monitoring_enabled: ['logs', 'metrics'], - // This will ensure the Fleet Server integration policy - // is also created and added to the agent policy - has_fleet_server: true, - }, - }) - .then((response) => response.data.item); - - log.indent(4); - log.info( - `Agent Policy created: ${createdFleetServerPolicy.name} (${createdFleetServerPolicy.id})` - ); - log.verbose(createdFleetServerPolicy); - log.indent(-4); - - return createdFleetServerPolicy.id; -}; - -const generateFleetServiceToken = async (): Promise => { - const { kbnClient, log } = getRuntimeServices(); - - const serviceToken: string = await kbnClient - .request({ - method: 'POST', - path: APP_API_ROUTES.GENERATE_SERVICE_TOKEN_PATTERN, - headers: { - 'elastic-api-version': API_VERSIONS.public.v1, - }, - body: {}, - }) - .then((response) => response.data.value); - - log.info(`New service token created.`); - - return serviceToken; -}; - -export const startFleetServerWithDocker = async ({ - policyId, - serviceToken, -}: { - policyId: string; - serviceToken: string; -}) => { - let containerId; - const { - log, - localhostRealIp, - elastic: { url: elasticUrl, isLocalhost: isElasticOnLocalhost }, - fleetServer: { port: fleetServerPort }, - kbnClient, - options: { version }, - } = getRuntimeServices(); - - log.info(`Starting a new fleet server using Docker`); - log.indent(4); - - const esURL = new URL(elasticUrl); - const containerName = `dev-fleet-server.${fleetServerPort}`; - let esUrlWithRealIp: string = elasticUrl; - - if (isElasticOnLocalhost) { - esURL.hostname = localhostRealIp; - esUrlWithRealIp = esURL.toString(); - } - - try { - const dockerArgs = [ - 'run', - '--restart', - 'no', - '--net', - 'elastic', - '--add-host', - 'host.docker.internal:host-gateway', - '--rm', - '--detach', - '--name', - containerName, - // The container's hostname will appear in Fleet when the agent enrolls - '--hostname', - containerName, - '--env', - 'FLEET_SERVER_ENABLE=1', - '--env', - `FLEET_SERVER_ELASTICSEARCH_HOST=${esUrlWithRealIp}`, - '--env', - `FLEET_SERVER_SERVICE_TOKEN=${serviceToken}`, - '--env', - `FLEET_SERVER_POLICY=${policyId}`, - '--publish', - `${fleetServerPort}:8220`, - `docker.elastic.co/beats/elastic-agent:${version}`, - ]; - - await execa('docker', ['kill', containerName]) - .then(() => { - log.verbose( - `Killed an existing container with name [${containerName}]. New one will be started.` - ); - }) - .catch((error) => { - log.verbose(`Attempt to kill currently running fleet-server container (if any) with name [${containerName}] was unsuccessful: - ${error} -(This is ok if one was not running already)`); - }); - - await addFleetServerHostToFleetSettings(`https://${localhostRealIp}:${fleetServerPort}`); - - log.verbose(`docker arguments:\n${dockerArgs.join(' ')}`); - - containerId = (await execa('docker', dockerArgs)).stdout; - - const fleetServerAgent = await waitForHostToEnroll(kbnClient, containerName, 120000); - - log.verbose(`Fleet server enrolled agent:\n${JSON.stringify(fleetServerAgent, null, 2)}`); - - log.info(`Done. Fleet Server is running and connected to Fleet. - Container Name: ${containerName} - Container Id: ${containerId} - - View running output: ${chalk.bold(`docker attach ---sig-proxy=false ${containerName}`)} - Shell access: ${chalk.bold(`docker exec -it ${containerName} /bin/bash`)} - Kill container: ${chalk.bold(`docker kill ${containerId}`)} -`); - } catch (error) { - log.error(dump(error)); - log.indent(-4); - throw error; - } - - log.indent(-4); - - return containerId; -}; - -export const startFleetServerStandAloneWithDocker = async () => { - let containerId; - const { - log, - elastic: { url: elasticUrl }, - fleetServer: { port: fleetServerPort }, - } = getRuntimeServices(); - - log.info(`Starting a new fleet server using Docker`); - log.indent(4); - const esURL = new URL(elasticUrl); - - esURL.hostname = SERVERLESS_NODES[0].name; - - const esUrlWithRealIp = esURL.toString(); - - const containerName = `dev-fleet-server.${fleetServerPort}`; - try { - const dockerArgs = [ - 'run', - '--restart', - 'no', - '--net', - 'elastic', - '--add-host', - 'host.docker.internal:host-gateway', - '--rm', - '--detach', - '--name', - containerName, - // The container's hostname will appear in Fleet when the agent enrolls - '--hostname', - containerName, - '--volume', - `${FLEET_SERVER_CERT_PATH}:/fleet-server.crt`, - '--volume', - `${FLEET_SERVER_KEY_PATH}:/fleet-server.key`, - '--env', - 'FLEET_SERVER_CERT=/fleet-server.crt', - '--env', - 'FLEET_SERVER_CERT_KEY=/fleet-server.key', - '--env', - `ELASTICSEARCH_HOSTS=${esUrlWithRealIp}`, - '--env', - `ELASTICSEARCH_SERVICE_TOKEN=${fleetServerDevServiceAccount.token}`, - '--env', - `ELASTICSEARCH_CA_TRUSTED_FINGERPRINT=${CA_TRUSTED_FINGERPRINT}`, - '--volume', - `${FLEET_SERVER_CUSTOM_CONFIG}:/etc/fleet-server.yml:ro`, - '--publish', - `${fleetServerPort}:8220`, - `docker.elastic.co/observability-ci/fleet-server:latest`, - ]; - - await execa('docker', ['kill', containerName]) - .then(() => { - log.verbose( - `Killed an existing container with name [${containerName}]. New one will be started.` - ); - }) - .catch((error) => { - log.verbose(`Attempt to kill currently running fleet-server container (if any) with name [${containerName}] was unsuccessful: - ${error} -(This is ok if one was not running already)`); - }); - - log.verbose(`docker arguments:\n${dockerArgs.join(' ')}`); - - containerId = (await execa('docker', dockerArgs)).stdout; - - log.info(`Done. Fleet Server Stand Alone is running and connected to Fleet. - Container Name: ${containerName} - Container Id: ${containerId} - - View running output: ${chalk.bold(`docker attach ---sig-proxy=false ${containerName}`)} - Shell access: ${chalk.bold(`docker exec -it ${containerName} /bin/bash`)} - Kill container: ${chalk.bold(`docker kill ${containerId}`)} -`); - } catch (error) { - log.error(dump(error)); - log.indent(-4); - throw error; - } - - log.indent(-4); - - return containerId; -}; - -const configureFleetIfNeeded = async () => { - const { log, kbnClient, localhostRealIp } = getRuntimeServices(); - - log.info('Checking if Fleet needs to be configured'); - log.indent(4); - - try { - // make sure that all ES hostnames are using localhost real IP - const fleetOutputs = await kbnClient - .request({ - method: 'GET', - headers: { - 'elastic-api-version': API_VERSIONS.public.v1, - }, - path: outputRoutesService.getListPath(), - }) - .then((response) => response.data); - - for (const { id, ...output } of fleetOutputs.items) { - if (output.type === 'elasticsearch') { - if (output.hosts) { - let needsUpdating = false; - const updatedHosts: Output['hosts'] = []; - - for (const host of output.hosts) { - const hostURL = new URL(host); - - if (isLocalhost(hostURL.hostname)) { - needsUpdating = true; - hostURL.hostname = localhostRealIp; - updatedHosts.push(hostURL.toString()); - - log.verbose( - `Fleet Settings for Elasticsearch Output [Name: ${ - output.name - } (id: ${id})]: Host [${host}] updated to [${hostURL.toString()}]` - ); - } else { - updatedHosts.push(host); - } - } - - if (needsUpdating) { - const update: PutOutputRequest['body'] = { - ...(output as PutOutputRequest['body']), // cast needed to quite TS - looks like the types for Output in fleet differ a bit between create/update - hosts: updatedHosts, - }; - - log.info(`Updating Fleet Settings for Output [${output.name} (${id})]`); - - await kbnClient - .request({ - method: 'PUT', - headers: { - 'elastic-api-version': API_VERSIONS.public.v1, - }, - path: outputRoutesService.getUpdatePath(id), - body: update, - }) - .catch(catchAxiosErrorFormatAndThrow); - } - } - } - } - } catch (error) { - log.error(dump(error)); - log.indent(-4); - throw error; - } - - log.indent(-4); -}; - -const addFleetServerHostToFleetSettings = async ( - fleetServerHostUrl: string -): Promise => { - const { kbnClient, log } = getRuntimeServices(); - - log.info(`Updating Fleet with new fleet server host: ${fleetServerHostUrl}`); - log.indent(4); - - try { - const exitingFleetServerHostUrl = await fetchFleetServerUrl(kbnClient); - - const newFleetHostEntry: PostFleetServerHostsRequest['body'] = { - name: `Dev fleet server running on localhost`, - host_urls: [fleetServerHostUrl], - is_default: !exitingFleetServerHostUrl, - }; - - const { item } = await kbnClient - .request({ - method: 'POST', - path: fleetServerHostsRoutesService.getCreatePath(), - headers: { - 'elastic-api-version': API_VERSIONS.public.v1, - }, - body: newFleetHostEntry, - }) - .catch(catchAxiosErrorFormatAndThrow) - .catch((error: FormattedAxiosError) => { - if ( - error.response.status === 403 && - ((error.response?.data?.message as string) ?? '').includes('disabled') - ) { - log.error(`Update failed with [403: ${error.response.data.message}]. - -${chalk.red('Are you running this utility against a Serverless project?')} -If so, the following entry should be added to your local -'config/serverless.[project_type].dev.yml' (ex. 'serverless.security.dev.yml'): - -${chalk.bold(chalk.cyan('xpack.fleet.internal.fleetServerStandalone: false'))} - -`); - } - - throw error; - }) - .then((response) => response.data); - - log.verbose(item); - log.indent(-4); - - return item; - } catch (error) { - log.error(dump(error)); - log.indent(-4); - throw error; - } -}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/index.ts b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/index.ts index 5e596c0c51a7..51fb0ea3b514 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/index.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/index.ts @@ -47,7 +47,6 @@ export const cli = () => { default: { kibanaUrl: 'http://127.0.0.1:5601', elasticUrl: 'http://127.0.0.1:9200', - fleetServerUrl: 'https://127.0.0.1:8220', username: 'elastic', password: 'changeme', version: '', @@ -65,7 +64,6 @@ export const cli = () => { --password Optional. Password associated with the username (Default: changeme) --kibanaUrl Optional. The url to Kibana (Default: http://127.0.0.1:5601) --elasticUrl Optional. The url to Elasticsearch (Default: http://127.0.0.1:9200) - --fleetServerUrl Optional. The url to Fleet Server (Default: https://127.0.0.1:8220) `, }, } diff --git a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/setup.ts b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/setup.ts index 18ef51a35bcc..c851b6ee34db 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/setup.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/setup.ts @@ -5,18 +5,23 @@ * 2.0. */ -import { runFleetServerIfNeeded } from './fleet_server'; -import { startRuntimeServices, stopRuntimeServices } from './runtime'; +import { getRuntimeServices, startRuntimeServices, stopRuntimeServices } from './runtime'; import { checkDependencies } from './pre_check'; import { enrollEndpointHost } from './elastic_endpoint'; import type { StartRuntimeServicesOptions } from './types'; +import { startFleetServerIfNecessary } from '../common/fleet_server/fleet_server_services'; export const setupAll = async (options: StartRuntimeServicesOptions) => { await startRuntimeServices(options); + const { kbnClient, log } = getRuntimeServices(); + await checkDependencies(); - await runFleetServerIfNeeded(); + await startFleetServerIfNecessary({ + kbnClient, + logger: log, + }); await enrollEndpointHost(); diff --git a/x-pack/plugins/security_solution/scripts/endpoint/sentinelone_host/index.ts b/x-pack/plugins/security_solution/scripts/endpoint/sentinelone_host/index.ts index a5aec7a52aec..3fad8bf0223b 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/sentinelone_host/index.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/sentinelone_host/index.ts @@ -7,27 +7,37 @@ import type { RunFn } from '@kbn/dev-cli-runner'; import { run } from '@kbn/dev-cli-runner'; -import { userInfo } from 'os'; import { ok } from 'assert'; +import { + isFleetServerRunning, + startFleetServer, +} from '../common/fleet_server/fleet_server_services'; +import type { HostVm } from '../common/types'; import { createToolingLogger } from '../../../common/endpoint/data_loaders/utils'; import { addSentinelOneIntegrationToAgentPolicy, + DEFAULT_AGENTLESS_INTEGRATIONS_AGENT_POLICY_NAME, + enrollHostVmWithFleet, + fetchAgentPolicy, getOrCreateDefaultAgentPolicy, } from '../common/fleet_services'; import { installSentinelOneAgent, S1Client } from './common'; -import { createVm } from '../common/vm_services'; -import { createRuntimeServices } from '../common/stack_services'; +import { createVm, generateVmName, getMultipassVmCountNotice } from '../common/vm_services'; +import { createKbnClient } from '../common/stack_services'; export const cli = async () => { // TODO:PT add support for CPU, Disk and Memory input args return run(runCli, { - description: - 'Creates a new VM and runs both SentinelOne agent and elastic agent with the SentinelOne integration', + description: `Sets up the kibana system so that SentinelOne hosts data can be be streamed to Elasticsearch. +It will first setup a host VM that runs the SentinelOne agent on it. This VM will ensure that data is being +created in SentinelOne. +It will then also setup a second VM (if necessary) that runs Elastic Agent along with the SentinelOne integration +policy (an agent-less integration) - this is the process that then connects to the SentinelOne management +console and pushes the data to Elasticsearch.`, flags: { string: [ 'kibanaUrl', - 'elasticUrl', 'username', 'password', 'version', @@ -36,29 +46,28 @@ export const cli = async () => { 's1ApiToken', 'vmName', ], - boolean: ['force'], + boolean: ['forceFleetServer'], default: { kibanaUrl: 'http://127.0.0.1:5601', - elasticUrl: 'http://127.0.0.1:9200', username: 'elastic', password: 'changeme', - version: '', policy: '', - force: false, }, help: ` --s1Url Required. The base URL for SentinelOne management console. Ex: https://usea1-partners.sentinelone.net (valid as of Oct. 2023) --s1ApiToken Required. The API token for SentinelOne - --vmName Optional. The name for the VM + --vmName Optional. The name for the VM. + Default: [current login user name]-sentinelone-[unique number] --policy Optional. The UUID of the Fleet Agent Policy that should be used to setup the SentinelOne Integration Default: re-uses existing dev policy (if found) or creates a new one + --forceFleetServer Optional. If fleet server should be started/configured even if it seems + like it is already setup. --username Optional. User name to be used for auth against elasticsearch and kibana (Default: elastic). --password Optional. Password associated with the username (Default: changeme) --kibanaUrl Optional. The url to Kibana (Default: http://127.0.0.1:5601) - --elasticUrl Optional. The url to Elasticsearch (Default: http://127.0.0.1:9200) `, }, }); @@ -68,38 +77,24 @@ const runCli: RunFn = async ({ log, flags }) => { const username = flags.username as string; const password = flags.password as string; const kibanaUrl = flags.kibanaUrl as string; - const elasticUrl = flags.elasticUrl as string; const s1Url = flags.s1Url as string; const s1ApiToken = flags.s1ApiToken as string; const policy = flags.policy as string; - - createToolingLogger.defaultLogLevel = flags.verbose - ? 'verbose' - : flags.debug - ? 'debug' - : flags.silent - ? 'silent' - : flags.quiet - ? 'error' - : 'info'; - + const forceFleetServer = flags.forceFleetServer as boolean; const getRequiredArgMessage = (argName: string) => `${argName} argument is required`; + createToolingLogger.setDefaultLogLevelFromCliFlags(flags); + ok(s1Url, getRequiredArgMessage('s1Url')); ok(s1ApiToken, getRequiredArgMessage('s1ApiToken')); - const vmName = - (flags.vmName as string) || - `${userInfo().username.toLowerCase().replaceAll('.', '-')}-sentinelone-${Math.random() - .toString() - .substring(2, 6)}`; + const vmName = (flags.vmName as string) || generateVmName('sentinelone'); const s1Client = new S1Client({ url: s1Url, apiToken: s1ApiToken, log }); - const { kbnClient } = await createRuntimeServices({ - kibanaUrl, - elasticsearchUrl: elasticUrl, + const kbnClient = createKbnClient({ + log, + url: kibanaUrl, username, password, - log, }); const hostVm = await createVm({ @@ -116,7 +111,17 @@ const runCli: RunFn = async ({ log, flags }) => { s1Client, }); - const agentPolicyId = policy || (await getOrCreateDefaultAgentPolicy({ kbnClient, log })).id; + const { + id: agentPolicyId, + agents = 0, + name: agentPolicyName, + } = policy + ? await fetchAgentPolicy(kbnClient, policy) + : await getOrCreateDefaultAgentPolicy({ + kbnClient, + log, + policyName: DEFAULT_AGENTLESS_INTEGRATIONS_AGENT_POLICY_NAME, + }); await addSentinelOneIntegrationToAgentPolicy({ kbnClient, @@ -126,10 +131,42 @@ const runCli: RunFn = async ({ log, flags }) => { apiToken: s1ApiToken, }); + let agentPolicyVm: HostVm | undefined; + + // If no agents are running against the given Agent policy for agentless integrations, then add one now + if (!agents) { + log.info(`Creating VM and enrolling it with Fleet using policy [${agentPolicyName}]`); + + agentPolicyVm = await createVm({ + type: 'multipass', + name: generateVmName('agentless-integrations'), + }); + + if (forceFleetServer || !(await isFleetServerRunning(kbnClient, log))) { + await startFleetServer({ + kbnClient, + logger: log, + force: forceFleetServer, + }); + } + + await enrollHostVmWithFleet({ + hostVm: agentPolicyVm, + kbnClient, + log, + agentPolicyId, + }); + } else { + log.info( + `No host VM created for Fleet agent policy [${agentPolicyName}]. It already shows to have [${agents}] enrolled` + ); + } + log.info(`Done! ${hostVm.info()} - +${agentPolicyVm ? `${agentPolicyVm.info()}\n` : ''} +${await getMultipassVmCountNotice(2)} SentinelOne Agent Status: ${s1Info.status} `); diff --git a/x-pack/plugins/security_solution/scripts/run_cypress/parallel.ts b/x-pack/plugins/security_solution/scripts/run_cypress/parallel.ts index b7cfabb2bc93..b5ab801c3fa2 100644 --- a/x-pack/plugins/security_solution/scripts/run_cypress/parallel.ts +++ b/x-pack/plugins/security_solution/scripts/run_cypress/parallel.ts @@ -10,7 +10,6 @@ import yargs from 'yargs'; import _ from 'lodash'; import globby from 'globby'; import pMap from 'p-map'; -import { ToolingLog } from '@kbn/tooling-log'; import { withProcRunner } from '@kbn/dev-proc-runner'; import cypress from 'cypress'; import { findChangedFiles } from 'find-cypress-specs'; @@ -27,18 +26,18 @@ import { import { createFailError } from '@kbn/dev-cli-errors'; import pRetry from 'p-retry'; +import { prefixedOutputLogger } from '../endpoint/common/utils'; +import { createToolingLogger } from '../../common/endpoint/data_loaders/utils'; +import { createKbnClient } from '../endpoint/common/stack_services'; +import type { StartedFleetServer } from '../endpoint/common/fleet_server/fleet_server_services'; +import { startFleetServer } from '../endpoint/common/fleet_server/fleet_server_services'; import { renderSummaryTable } from './print_run'; import { parseTestFileConfig, retrieveIntegrations } from './utils'; import { getFTRConfig } from './get_ftr_config'; export const cli = () => { run( - async () => { - const log = new ToolingLog({ - level: 'info', - writeTo: process.stdout, - }); - + async ({ log: _cliLogger }) => { const { argv } = yargs(process.argv.slice(2)) .coerce('configFile', (arg) => (_.isArray(arg) ? _.last(arg) : arg)) .coerce('spec', (arg) => (_.isArray(arg) ? _.last(arg) : arg)) @@ -55,7 +54,7 @@ export const cli = () => { ) .boolean('inspect'); - log.info(` + _cliLogger.info(` ---------------------------------------------- Script arguments: ---------------------------------------------- @@ -66,10 +65,15 @@ ${JSON.stringify(argv, null, 2)} `); const isOpen = argv._.includes('open'); - const cypressConfigFilePath = require.resolve(`../../${argv.configFile}`) as string; const cypressConfigFile = await import(cypressConfigFilePath); + if (cypressConfigFile.env?.TOOLING_LOG_LEVEL) { + createToolingLogger.defaultLogLevel = cypressConfigFile.env.TOOLING_LOG_LEVEL; + } + + const log = prefixedOutputLogger('cy.parallel()', createToolingLogger()); + log.info(` ---------------------------------------------- Cypress config for file: ${cypressConfigFilePath}: @@ -234,6 +238,34 @@ ${JSON.stringify(cypressConfigFile, null, 2)} isOpen, }); + const createUrlFromFtrConfig = ( + type: 'elasticsearch' | 'kibana' | 'fleetserver', + withAuth: boolean = false + ): string => { + const getKeyPath = (keyPath: string = ''): string => { + return `servers.${type}${keyPath ? `.${keyPath}` : ''}`; + }; + + if (!config.get(getKeyPath())) { + throw new Error(`Unable to create URL for ${type}. Not found in FTR config at `); + } + + const url = new URL('http://localhost'); + + url.port = config.get(getKeyPath('port')); + url.protocol = config.get(getKeyPath('protocol')); + url.hostname = config.get(getKeyPath('hostname')); + + if (withAuth) { + url.username = config.get(getKeyPath('username')); + url.password = config.get(getKeyPath('password')); + } + + return url.toString().replace(/\/$/, ''); + }; + + const baseUrl = createUrlFromFtrConfig('kibana'); + log.info(` ---------------------------------------------- Cypress FTR setup for file: ${filePath}: @@ -294,6 +326,31 @@ ${JSON.stringify( inspect: argv.inspect, }); + // Setup fleet if Cypress config requires it + let fleetServer: void | StartedFleetServer; + if (cypressConfigFile.env?.WITH_FLEET_SERVER) { + log.info(`Setting up fleet-server for this Cypress config`); + + const kbnClient = createKbnClient({ + url: baseUrl, + username: config.get('servers.kibana.username'), + password: config.get('servers.kibana.password'), + log, + }); + + fleetServer = await startFleetServer({ + kbnClient, + logger: log, + port: + fleetServerPort ?? config.has('servers.fleetserver.port') + ? (config.get('servers.fleetserver.port') as number) + : undefined, + // `force` is needed to ensure that any currently running fleet server (perhaps left + // over from an interrupted run) is killed and a new one restarted + force: true, + }); + } + await providers.loadAll(); const functionalTestRunner = new FunctionalTestRunner( @@ -302,34 +359,6 @@ ${JSON.stringify( EsVersion.getDefault() ); - const createUrlFromFtrConfig = ( - type: 'elasticsearch' | 'kibana' | 'fleetserver', - withAuth: boolean = false - ): string => { - const getKeyPath = (keyPath: string = ''): string => { - return `servers.${type}${keyPath ? `.${keyPath}` : ''}`; - }; - - if (!config.get(getKeyPath())) { - throw new Error(`Unable to create URL for ${type}. Not found in FTR config at `); - } - - const url = new URL('http://localhost'); - - url.port = config.get(getKeyPath('port')); - url.protocol = config.get(getKeyPath('protocol')); - url.hostname = config.get(getKeyPath('hostname')); - - if (withAuth) { - url.username = config.get(getKeyPath('username')); - url.password = config.get(getKeyPath('password')); - } - - return url.toString().replace(/\/$/, ''); - }; - - const baseUrl = createUrlFromFtrConfig('kibana'); - const ftrEnv = await pRetry(() => functionalTestRunner.run(abortCtrl.signal), { retries: 1, }); @@ -408,6 +437,10 @@ ${JSON.stringify(cyCustomEnv, null, 2)} } } + if (fleetServer) { + await fleetServer.stop(); + } + await procs.stop('kibana'); await shutdownEs(); cleanupServerPorts({ esPort, kibanaPort, fleetServerPort }); diff --git a/x-pack/test/osquery_cypress/agent.ts b/x-pack/test/osquery_cypress/agent.ts index cd7969703c48..63429fb4e5b7 100644 --- a/x-pack/test/osquery_cypress/agent.ts +++ b/x-pack/test/osquery_cypress/agent.ts @@ -8,7 +8,10 @@ import execa from 'execa'; import { ToolingLog } from '@kbn/tooling-log'; import { KbnClient } from '@kbn/test'; -import { waitForHostToEnroll } from '@kbn/security-solution-plugin/scripts/endpoint/common/fleet_services'; +import { + fetchFleetServerUrl, + waitForHostToEnroll, +} from '@kbn/security-solution-plugin/scripts/endpoint/common/fleet_services'; import { getLatestVersion } from './artifact_manager'; import { Manager } from './resource_manager'; @@ -40,6 +43,9 @@ export class AgentManager extends Manager { const artifact = `docker.elastic.co/beats/elastic-agent:${await getLatestVersion()}`; this.log.info(artifact); const containerName = generateRandomString(12); + const fleetServerUrl = + (await fetchFleetServerUrl(this.kbnClient)) ?? + `https://host.docker.internal:${this.fleetServerPort}`; const dockerArgs = [ 'run', @@ -55,7 +61,7 @@ export class AgentManager extends Manager { '--env', 'FLEET_ENROLL=1', '--env', - `FLEET_URL=https://host.docker.internal:${this.fleetServerPort}`, + `FLEET_URL=${fleetServerUrl}`, '--env', `FLEET_ENROLLMENT_TOKEN=${this.policyEnrollmentKey}`, '--env', @@ -64,8 +70,12 @@ export class AgentManager extends Manager { artifact, ]; - this.agentContainerId = (await execa('docker', dockerArgs)).stdout; - await waitForHostToEnroll(this.kbnClient, containerName); + const startedContainer = await execa('docker', dockerArgs); + + this.log.info(`agent docker container started:\n${JSON.stringify(startedContainer, null, 2)}`); + + this.agentContainerId = startedContainer.stdout; + await waitForHostToEnroll(this.kbnClient, this.log, containerName, 240000); } public cleanup() { diff --git a/x-pack/test/osquery_cypress/fleet_server.ts b/x-pack/test/osquery_cypress/fleet_server.ts index f1fa7a174ae3..264bf9f86989 100644 --- a/x-pack/test/osquery_cypress/fleet_server.ts +++ b/x-pack/test/osquery_cypress/fleet_server.ts @@ -6,40 +6,52 @@ */ import { ToolingLog } from '@kbn/tooling-log'; -import execa from 'execa'; -import { runFleetServerIfNeeded } from '@kbn/security-solution-plugin/scripts/endpoint/endpoint_agent_runner/fleet_server'; +import { KbnClient } from '@kbn/test'; +import { + StartedFleetServer, + startFleetServer, +} from '@kbn/security-solution-plugin/scripts/endpoint/common/fleet_server/fleet_server_services'; import { Manager } from './resource_manager'; +import { getLatestAvailableAgentVersion } from './utils'; export class FleetManager extends Manager { - private fleetContainerId?: string; - private log: ToolingLog; + private fleetServer: StartedFleetServer | undefined = undefined; - constructor(log: ToolingLog) { + constructor( + private readonly kbnClient: KbnClient, + private readonly log: ToolingLog, + private readonly port: number + ) { super(); - this.log = log; } public async setup(): Promise { - const fleetServerConfig = await runFleetServerIfNeeded(); - - if (!fleetServerConfig) { - throw new Error('Fleet server config not found'); + const version = await getLatestAvailableAgentVersion(this.kbnClient); + this.fleetServer = await startFleetServer({ + kbnClient: this.kbnClient, + logger: this.log, + port: this.port, + force: true, + version, + }); + + if (!this.fleetServer) { + throw new Error('Fleet server was not started'); } - - this.fleetContainerId = fleetServerConfig.fleetServerContainerId; } public cleanup() { super.cleanup(); this.log.info('Removing old fleet config'); - if (this.fleetContainerId) { + if (this.fleetServer) { this.log.info('Closing fleet process'); try { - execa.sync('docker', ['kill', this.fleetContainerId]); + this.fleetServer.stopNow(); } catch (err) { this.log.error('Error closing fleet server process'); + this.log.verbose(err); } this.log.info('Fleet server process closed'); } diff --git a/x-pack/test/osquery_cypress/runner.ts b/x-pack/test/osquery_cypress/runner.ts index 7e7ac5e652fd..486305a41cfc 100644 --- a/x-pack/test/osquery_cypress/runner.ts +++ b/x-pack/test/osquery_cypress/runner.ts @@ -8,45 +8,30 @@ import Url from 'url'; import { verifyDockerInstalled, maybeCreateDockerNetwork } from '@kbn/es'; -import { startRuntimeServices } from '@kbn/security-solution-plugin/scripts/endpoint/endpoint_agent_runner/runtime'; +import { createToolingLogger } from '@kbn/security-solution-plugin/common/endpoint/data_loaders/utils'; +import { prefixedOutputLogger } from '@kbn/security-solution-plugin/scripts/endpoint/common/utils'; import { FtrProviderContext } from './ftr_provider_context'; import { AgentManager } from './agent'; import { FleetManager } from './fleet_server'; -import { createAgentPolicy, getLatestAvailableAgentVersion } from './utils'; +import { createAgentPolicy } from './utils'; async function setupFleetAgent({ getService }: FtrProviderContext) { - const log = getService('log'); + // Un-comment line below to set tooling log levels to verbose. Useful when debugging + // createToolingLogger.defaultLogLevel = 'verbose'; + + // const log = getService('log'); const config = getService('config'); const kbnClient = getService('kibanaServer'); - const elasticUrl = Url.format(config.get('servers.elasticsearch')); - const kibanaUrl = Url.format(config.get('servers.kibana')); - const fleetServerUrl = Url.format({ - protocol: config.get('servers.kibana.protocol'), - hostname: config.get('servers.kibana.hostname'), - port: config.get('servers.fleetserver.port'), - }); - const username = config.get('servers.elasticsearch.username'); - const password = config.get('servers.elasticsearch.password'); + const log = prefixedOutputLogger('cy.OSQuery', createToolingLogger()); await verifyDockerInstalled(log); await maybeCreateDockerNetwork(log); + await new FleetManager(kbnClient, log, config.get('servers.fleetserver.port')).setup(); - await startRuntimeServices({ - log, - elasticUrl, - kibanaUrl, - fleetServerUrl, - username, - password, - version: await getLatestAvailableAgentVersion(kbnClient), - }); - - await new FleetManager(log).setup(); - - const policyEnrollmentKey = await createAgentPolicy(kbnClient, log, 'Default policy'); - const policyEnrollmentKeyTwo = await createAgentPolicy(kbnClient, log, 'Osquery policy'); + const policyEnrollmentKey = await createAgentPolicy(kbnClient, log, `Default policy`); + const policyEnrollmentKeyTwo = await createAgentPolicy(kbnClient, log, `Osquery policy`); const port = config.get('servers.fleetserver.port');