diff --git a/packages/kbn-test/src/es/test_es_cluster.ts b/packages/kbn-test/src/es/test_es_cluster.ts index 9d37e60fdff0f..461ad2b6f0df1 100644 --- a/packages/kbn-test/src/es/test_es_cluster.ts +++ b/packages/kbn-test/src/es/test_es_cluster.ts @@ -71,7 +71,7 @@ export interface CreateTestEsClusterOptions { */ esArgs?: string[]; esFrom?: string; - esServerlessOptions?: Pick; + esServerlessOptions?: Pick; esJavaOpts?: string; /** * License to run your cluster under. Keep in mind that a `trial` license @@ -245,6 +245,7 @@ export function createTestEsCluster< image: esServerlessOptions?.image, tag: esServerlessOptions?.tag, host: esServerlessOptions?.host, + resources: esServerlessOptions?.resources, port, clean: true, background: true, diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 6c037bab1c3e5..2f4cb317d5615 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -217,6 +217,7 @@ export const schema = Joi.object() esServerlessOptions: Joi.object() .keys({ host: Joi.string().ip(), + resources: Joi.array().items(Joi.string()).default([]), }) .default(), diff --git a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts index 2ad6725e8258b..742f729745d27 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts +++ b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts @@ -166,17 +166,22 @@ function getESServerlessOptions(esServerlessImageFromArg: string | undefined, co esTestConfig.getESServerlessImage() || (config.has('esTestCluster.esServerlessImage') && config.get('esTestCluster.esServerlessImage')); + const serverlessResources: string[] = + (config.has('esServerlessOptions.resources') && config.get('esServerlessOptions.resources')) || + []; const serverlessHost: string | undefined = config.has('esServerlessOptions.host') && config.get('esServerlessOptions.host'); if (esServerlessImageUrlOrTag) { if (esServerlessImageUrlOrTag.includes(':')) { return { + resources: serverlessResources, image: esServerlessImageUrlOrTag, host: serverlessHost, }; } else { return { + resources: serverlessResources, tag: esServerlessImageUrlOrTag, host: serverlessHost, }; @@ -184,6 +189,7 @@ function getESServerlessOptions(esServerlessImageFromArg: string | undefined, co } return { + resources: serverlessResources, host: serverlessHost, }; } diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/artifact_tabs_in_policy_details.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/artifact_tabs_in_policy_details.cy.ts index 8610ac1fc3ba5..a17024a0dbc38 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/artifact_tabs_in_policy_details.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/artifact_tabs_in_policy_details.cy.ts @@ -60,7 +60,8 @@ const visitArtifactTab = (tabId: string) => { describe( 'Artifact tabs in Policy Details page', - { tags: ['@ess', '@serverless', '@brokenInServerless'] }, // broken due to disabled Native Role Management + // FIXME: Test needs to be refactored for serverless so that it uses a standard set of users that are also available in serverless + { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => { before(() => { login(); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/no_license.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/no_license.cy.ts index eb6191ece0460..192a4fd853bd5 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/no_license.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/no_license.cy.ts @@ -34,7 +34,8 @@ describe('No License', { tags: '@ess', env: { ftrConfig: { license: 'basic' } } }); }); - describe('User cannot see results', () => { + // FIXME: Flaky. Needs fixing (security team issue #7763) + describe.skip('User cannot see results', () => { let endpointData: ReturnTypeFromChainable | undefined; let alertData: ReturnTypeFromChainable | undefined; const [endpointAgentId, endpointHostname] = generateRandomStringName(2); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_alerts.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_alerts.cy.ts index 3daf711eca9cd..d79d27a774eac 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_alerts.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_alerts.cy.ts @@ -19,7 +19,8 @@ import { login } from '../tasks/login'; import { EXECUTE_ROUTE } from '../../../../common/endpoint/constants'; import { waitForActionToComplete } from '../tasks/response_actions'; -describe( +// FIXME: Flaky. Needs fixing (security team issue #7763) +describe.skip( 'Endpoint generated alerts', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => { diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_role_rbac.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_role_rbac.cy.ts index a09f9e8b8273d..d0f9da6280c8d 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_role_rbac.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint_role_rbac.cy.ts @@ -6,7 +6,7 @@ */ import { closeAllToasts } from '../tasks/toasts'; -import { login } from '../tasks/login'; +import { login, ROLE } from '../tasks/login'; import { loadPage } from '../tasks/common'; describe('When defining a kibana role for Endpoint security access', { tags: '@ess' }, () => { @@ -18,7 +18,7 @@ describe('When defining a kibana role for Endpoint security access', { tags: '@e }; beforeEach(() => { - login(); + login(ROLE.system_indices_superuser); loadPage('/app/management/security/roles/edit'); closeAllToasts(); cy.getByTestSubj('addSpacePrivilegeButton').click(); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/endpoint_list_with_security_essentials.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/endpoint_list_with_security_essentials.cy.ts index 32800978a968e..9cd298535df26 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/endpoint_list_with_security_essentials.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/endpoint_list_with_security_essentials.cy.ts @@ -18,7 +18,7 @@ import { describe( 'When on the Endpoint List in Security Essentials PLI', { - tags: ['@serverless', '@brokenInServerless'], + tags: ['@serverless'], env: { ftrConfig: { productTypes: [{ product_line: 'security', product_tier: 'essentials' }], diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/complete.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/complete.cy.ts index dba7166b9a9e8..6667677bda7b7 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/complete.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/complete.cy.ts @@ -14,7 +14,7 @@ import { getEndpointManagementPageList } from '../../../screens'; describe( 'App Features for Security Complete PLI', { - tags: ['@serverless', '@brokenInServerless'], + tags: ['@serverless'], env: { ftrConfig: { productTypes: [{ product_line: 'security', product_tier: 'complete' }] }, }, diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/complete_with_endpoint.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/complete_with_endpoint.cy.ts index 3c028b9e25040..e00a266f600b9 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/complete_with_endpoint.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/complete_with_endpoint.cy.ts @@ -17,7 +17,7 @@ import { describe( 'App Features for Security Complete PLI with Endpoint Complete Addon', { - tags: ['@serverless', '@brokenInServerless'], + tags: ['@serverless'], env: { ftrConfig: { productTypes: [ diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/essentials.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/essentials.cy.ts index fed4494722df5..d0fdcf633814a 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/essentials.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/essentials.cy.ts @@ -14,7 +14,7 @@ import { getEndpointManagementPageList } from '../../../screens'; describe( 'App Features for Security Essential PLI', { - tags: ['@serverless', '@brokenInServerless'], + tags: ['@serverless'], env: { ftrConfig: { productTypes: [{ product_line: 'security', product_tier: 'essentials' }], diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/essentials_with_endpoint.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/essentials_with_endpoint.cy.ts index 172f850e44b7c..786f4f20ad25b 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/essentials_with_endpoint.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/essentials_with_endpoint.cy.ts @@ -17,7 +17,7 @@ import { describe( 'App Features for Security Essentials PLI with Endpoint Essentials Addon', { - tags: ['@serverless', '@brokenInServerless'], + tags: ['@serverless'], env: { ftrConfig: { productTypes: [ diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/policy_details_with_security_essentials.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/policy_details_with_security_essentials.cy.ts index e1516bb08e10e..1cb0c382ca707 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/policy_details_with_security_essentials.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/policy_details_with_security_essentials.cy.ts @@ -12,7 +12,7 @@ import type { IndexedFleetEndpointPolicyResponse } from '../../../../../common/e describe( 'When displaying the Policy Details in Security Essentials PLI', { - tags: ['@serverless', '@brokenInServerless'], + tags: ['@serverless'], env: { ftrConfig: { productTypes: [{ product_line: 'security', product_tier: 'essentials' }], diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/roles/complete_with_endpoint_roles.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/roles/complete_with_endpoint_roles.cy.ts index 7d872de49062d..a197574035b79 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/roles/complete_with_endpoint_roles.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/roles/complete_with_endpoint_roles.cy.ts @@ -33,7 +33,7 @@ import { describe( 'User Roles for Security Complete PLI with Endpoint Complete addon', { - tags: ['@serverless', '@brokenInServerless'], + tags: ['@serverless'], env: { ftrConfig: { productTypes: [ diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/roles/essentials_with_endpoint.roles.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/roles/essentials_with_endpoint.roles.cy.ts index dec6018bddc3c..4e0ae2080a97b 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/roles/essentials_with_endpoint.roles.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/roles/essentials_with_endpoint.roles.cy.ts @@ -26,7 +26,7 @@ import { describe( 'Roles for Security Essential PLI with Endpoint Essentials addon', { - tags: ['@serverless', '@brokenInServerless'], + tags: ['@serverless'], env: { ftrConfig: { productTypes: [ diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/login.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/login.ts index 77987b5fd76ed..a15d01c54049d 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/login.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/login.ts @@ -44,8 +44,7 @@ interface CyLoginTask { * @param user */ export const login: CyLoginTask = ( - // FIXME:PT default user to `soc_manager` - user?: SecurityTestUser + user: SecurityTestUser = ROLE.endpoint_operations_analyst ): ReturnType => { let username = Cypress.env('KIBANA_USERNAME'); let password = Cypress.env('KIBANA_PASSWORD'); diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts index 47f6da88c6924..8f4f1e797910b 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts @@ -172,7 +172,6 @@ export const ensureResponseActionAuthzAccess = ( { const file = new File(['foo'], 'foo.txt'); const formData = new FormData(); - formData.append('file', file, file.name); for (const [key, value] of Object.entries(apiPayload as object)) { @@ -199,6 +198,8 @@ export const ensureResponseActionAuthzAccess = ( }, failOnStatusCode: false, body: apiPayload as Cypress.RequestBody, + // Increased timeout due to `upload` action. It seems to take much longer to complete due to file upload + timeout: 120000, }; if (accessLevel === 'none') { diff --git a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/fleet_server.yml b/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_server/fleet_server.yml similarity index 100% rename from x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/fleet_server.yml rename to x-pack/plugins/security_solution/scripts/endpoint/common/fleet_server/fleet_server.yml 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 new file mode 100644 index 0000000000000..4357adeeaf6cd --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_server/fleet_server_services.ts @@ -0,0 +1,674 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ToolingLog } from '@kbn/tooling-log'; +import type { KbnClient } from '@kbn/test'; +import execa from 'execa'; +import chalk from 'chalk'; +import assert from 'assert'; +import type { AgentPolicy, CreateAgentPolicyResponse, Output } from '@kbn/fleet-plugin/common'; +import { + AGENT_POLICY_API_ROUTES, + API_VERSIONS, + FLEET_SERVER_PACKAGE, + FLEET_SERVER_SERVERS_INDEX, + PACKAGE_POLICY_SAVED_OBJECT_TYPE, +} from '@kbn/fleet-plugin/common'; +import type { + FleetServerHost, + GetOneOutputResponse, + PutOutputRequest, +} from '@kbn/fleet-plugin/common/types'; +import type { + PostFleetServerHostsRequest, + PostFleetServerHostsResponse, +} from '@kbn/fleet-plugin/common/types/rest_spec/fleet_server_hosts'; +import { + fleetServerHostsRoutesService, + outputRoutesService, +} from '@kbn/fleet-plugin/common/services'; +import axios from 'axios'; +import * as https from 'https'; +import { + CA_TRUSTED_FINGERPRINT, + FLEET_SERVER_CERT_PATH, + FLEET_SERVER_KEY_PATH, + fleetServerDevServiceAccount, +} from '@kbn/dev-utils'; +import { maybeCreateDockerNetwork, SERVERLESS_NODES, verifyDockerInstalled } from '@kbn/es'; +import { resolve } from 'path'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { + RETRYABLE_TRANSIENT_ERRORS, + retryOnError, +} from '../../../../common/endpoint/data_loaders/utils'; +import { isServerlessKibanaFlavor } from '../stack_services'; +import type { FormattedAxiosError } from '../format_axios_error'; +import { catchAxiosErrorFormatAndThrow } from '../format_axios_error'; +import { + fetchFleetOutputs, + fetchFleetServerHostList, + fetchFleetServerUrl, + fetchIntegrationPolicyList, + generateFleetServiceToken, + getAgentVersionMatchingCurrentStack, + getFleetElasticsearchOutputHost, + waitForHostToEnroll, +} from '../fleet_services'; +import { dump } from '../../endpoint_agent_runner/utils'; +import { getLocalhostRealIp } from '../network_services'; +import { isLocalhost } from '../is_localhost'; + +export const FLEET_SERVER_CUSTOM_CONFIG = resolve(__dirname, './fleet_server.yml'); + +interface StartedServer { + /** The type of virtualization used to start the server */ + type: 'docker'; + /** The ID of the server */ + id: string; + /** The name of the server */ + name: string; + /** The url (including port) to the server */ + url: string; + /** Stop server */ + stop: () => Promise; + /** Any information about the server */ + info?: string; +} + +interface StartFleetServerOptions { + kbnClient: KbnClient; + logger: ToolingLog; + /** Policy ID that should be used to enroll the fleet-server agent */ + policy?: string; + /** Agent version */ + version?: string; + /** Will start fleet-server even if its detected that it is already running */ + force?: boolean; + /** The port number that will be used by fleet-server to listen for requests */ + port?: number; +} + +interface StartedFleetServer extends StartedServer { + /** The policy id that the fleet-server agent is running with */ + policyId: string; +} + +export const startFleetServer = async ({ + kbnClient, + logger, + policy, + version, + force = false, + port = 8220, +}: StartFleetServerOptions): Promise => { + logger.info(`Starting Fleet Server and connecting it to Kibana`); + + const response = await logger.indent(4, async () => { + const isServerless = await isServerlessKibanaFlavor(kbnClient); + + // Check if fleet already running if `force` is false + if (!force) { + const currentFleetServerUrl = await fetchFleetServerUrl(kbnClient); + + if (currentFleetServerUrl && (await isFleetServerRunning(currentFleetServerUrl))) { + throw new Error( + `Fleet server is already configured for this instance of Kibana and available at: ${currentFleetServerUrl}.\n(Use 'force' option to bypass this error)` + ); + } + } + + // Only fetch/create a fleet-server policy + const policyId = + policy ?? !isServerless ? await getOrCreateFleetServerAgentPolicyId(kbnClient, logger) : ''; + const serviceToken = isServerless ? '' : await generateFleetServiceToken(kbnClient, logger); + const startedFleetServer = await startFleetServerWithDocker({ + kbnClient, + logger, + policyId, + version, + port, + serviceToken, + }); + + return { + ...startedFleetServer, + policyId, + }; + }); + + return response; +}; + +const getOrCreateFleetServerAgentPolicyId = async ( + kbnClient: KbnClient, + log: ToolingLog +): Promise => { + log.info(`Retrieving/creating Fleet Server agent policy`); + + return log.indent(4, async () => { + const existingFleetServerIntegrationPolicy = await fetchIntegrationPolicyList(kbnClient, { + perPage: 1, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: "${FLEET_SERVER_PACKAGE}"`, + }).then((response) => response.items[0]); + + 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': '2023-10-31' }, + 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) + .catch(catchAxiosErrorFormatAndThrow); + + log.info( + `Agent Policy created: ${createdFleetServerPolicy.name} (${createdFleetServerPolicy.id})` + ); + log.verbose(createdFleetServerPolicy); + + return createdFleetServerPolicy.id; + }); +}; + +interface StartFleetServerWithDockerOptions { + kbnClient: KbnClient; + logger: ToolingLog; + /** The agent policy id. Required for non-serverless env. */ + policyId?: string; + /** The service token for fleet server. Required for non-serverless env. */ + serviceToken?: string; + version?: string; + port?: number; +} + +const startFleetServerWithDocker = async ({ + kbnClient, + logger: log, + policyId = '', + serviceToken = '', + version, + port = 8220, +}: StartFleetServerWithDockerOptions): Promise => { + await verifyDockerInstalled(log); + + let agentVersion = version || (await getAgentVersionMatchingCurrentStack(kbnClient)); + + log.info(`Starting a new fleet server using Docker (version: ${agentVersion})`); + + 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)}`; + let containerId = ''; + + if (isLocalhost(esURL.hostname)) { + esURL.hostname = localhostRealIp; + } + + if (isServerless) { + log.info(`Kibana running in serverless mode. + - will install/run standalone Fleet Server + - version adjusted to [latest] from [${agentVersion}]`); + + agentVersion = 'latest'; + await maybeCreateDockerNetwork(log); + } else { + assert.ok(!!policyId, '`policyId` is required'); + assert.ok(!!serviceToken, '`serviceToken` is required'); + } + + try { + const dockerArgs = isServerless + ? getFleetServerStandAloneDockerArgs({ + containerName, + hostname, + port, + esUrl: esURL.toString(), + agentVersion, + }) + : getFleetServerManagedDockerArgs({ + containerName, + hostname, + port, + serviceToken, + policyId, + agentVersion, + esUrl: esURL.toString(), + }); + + 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(`Fleet server started`); + + await addFleetServerHostToFleetSettings(kbnClient, log, fleetServerUrl); + await updateFleetElasticsearchOutputHostNames(kbnClient, log); + + if (isServerless) { + log.info(`Waiting for server to register with Elasticsearch`); + + 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)}`); + } + } catch (error) { + log.error(dump(error)); + throw error; + } + + const info = `Container Name: ${containerName} +Container Id: ${containerId} + +View running output: ${chalk.cyan(`docker attach ---sig-proxy=false ${containerName}`)} +Shell access: ${chalk.cyan(`docker exec -it ${containerName} /bin/bash`)} +Kill container: ${chalk.cyan(`docker kill ${containerId}`)} + `; + + return { + type: 'docker', + name: containerName, + id: containerId, + url: fleetServerUrl, + info, + stop: async () => { + await execa('docker', ['kill', containerId]); + }, + }; + }); + + log.info(`Done. Fleet server up and running`); + + return response; +}; + +interface GetFleetServerManagedDockerArgsOptions { + containerName: string; + esUrl: string; + serviceToken: string; + policyId: string; + port: number; + agentVersion: string; + /** The hostname. Defaults to `containerName` */ + hostname?: string; +} + +const getFleetServerManagedDockerArgs = ({ + hostname, + port, + serviceToken, + esUrl, + containerName, + agentVersion, + policyId, +}: GetFleetServerManagedDockerArgsOptions): string[] => { + return [ + 'run', + + '--restart', + 'no', + + '--add-host', + 'host.docker.internal:host-gateway', + + '--rm', + + '--detach', + + '--name', + containerName, + + // The container's hostname will appear in Fleet when the agent enrolls + '--hostname', + hostname || containerName, + + '--env', + 'FLEET_SERVER_ENABLE=1', + + '--env', + `FLEET_SERVER_ELASTICSEARCH_HOST=${esUrl}`, + + '--env', + `FLEET_SERVER_SERVICE_TOKEN=${serviceToken}`, + + '--env', + `FLEET_SERVER_POLICY=${policyId}`, + + '--publish', + `${port}:8220`, + + `docker.elastic.co/beats/elastic-agent:${agentVersion}`, + ]; +}; + +type GetFleetServerStandAloneDockerArgsOptions = Pick< + GetFleetServerManagedDockerArgsOptions, + 'esUrl' | 'hostname' | 'containerName' | 'port' | 'agentVersion' +>; + +const getFleetServerStandAloneDockerArgs = ({ + containerName, + hostname, + esUrl, + agentVersion, + port, +}: GetFleetServerStandAloneDockerArgsOptions): string[] => { + const esURL = new URL(esUrl); + esURL.hostname = SERVERLESS_NODES[0].name; + + return [ + 'run', + + '--restart', + 'no', + + '--net', + 'elastic', + + '--add-host', + 'host.docker.internal:host-gateway', + + '--rm', + '--detach', + + '--name', + containerName, + + // The hostname will appear in Fleet when the agent enrolls + '--hostname', + hostname || containerName, + + '--env', + 'FLEET_SERVER_CERT=/fleet-server.crt', + + '--env', + 'FLEET_SERVER_CERT_KEY=/fleet-server.key', + + '--env', + `ELASTICSEARCH_HOSTS=${esURL.toString()}`, + + '--env', + `ELASTICSEARCH_SERVICE_TOKEN=${fleetServerDevServiceAccount.token}`, + + '--env', + `ELASTICSEARCH_CA_TRUSTED_FINGERPRINT=${CA_TRUSTED_FINGERPRINT}`, + + '--volume', + `${FLEET_SERVER_CERT_PATH}:/fleet-server.crt`, + + '--volume', + `${FLEET_SERVER_KEY_PATH}:/fleet-server.key`, + + '--volume', + `${FLEET_SERVER_CUSTOM_CONFIG}:/etc/fleet-server.yml:ro`, + + '--publish', + `${port}:8220`, + + `docker.elastic.co/observability-ci/fleet-server:${agentVersion}`, + ]; +}; + +const addFleetServerHostToFleetSettings = async ( + kbnClient: KbnClient, + log: ToolingLog, + fleetServerHostUrl: string +): Promise => { + log.info(`Updating Fleet with new fleet server host: ${fleetServerHostUrl}`); + + return log.indent(4, async () => { + try { + const exitingFleetServerHostList = await fetchFleetServerHostList(kbnClient); + + // If the fleet server URL is already configured, then do nothing and exit + for (const fleetServerEntry of exitingFleetServerHostList.items) { + if (fleetServerEntry.host_urls.includes(fleetServerHostUrl)) { + log.info('No update needed. Fleet server host URL already defined in fleet settings.'); + return fleetServerEntry; + } + } + + const newFleetHostEntry: PostFleetServerHostsRequest['body'] = { + name: `Dev fleet server running on localhost`, + host_urls: [fleetServerHostUrl], + is_default: !exitingFleetServerHostList.total, + }; + + 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(`Attempt to update fleet server host URL in fleet 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.info(`Fleet settings updated with fleet host URL successful`); + return item; + } catch (error) { + log.error(dump(error)); + throw error; + } + }); +}; + +const updateFleetElasticsearchOutputHostNames = async ( + kbnClient: KbnClient, + log: ToolingLog +): Promise => { + log.info('Checking if Fleet output for Elasticsearch needs to be updated'); + + return log.indent(4, async () => { + try { + const localhostRealIp = getLocalhostRealIp(); + const fleetOutputs = await fetchFleetOutputs(kbnClient); + + // make sure that all ES hostnames are using localhost real IP + 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': '2023-10-31' }, + path: outputRoutesService.getUpdatePath(id), + body: update, + }) + .catch(catchAxiosErrorFormatAndThrow); + } + } + } + } + } catch (error) { + log.error(dump(error)); + throw error; + } + }); +}; + +/** + * Checks to see if the fleet server at the given URL is up and running by calling + * the status api + * @param serverUrl + */ +export const isFleetServerRunning = async (serverUrl: string): Promise => { + const url = new URL(serverUrl); + url.pathname = '/api/status'; + + return axios + .request({ + method: 'GET', + url: url.toString(), + responseType: 'json', + // Custom agent to ensure we don't get cert errors + httpsAgent: new https.Agent({ rejectUnauthorized: false }), + }) + .then(() => { + return true; + }) + .catch(() => { + return false; + }); +}; + +/** + * Checks and waits until the given fleet server hostname has been registered into elasticsearch. + * This check can be used when enrolling a standalone fleet-server, since those would not show up + * in Kibana's Fleet UI. + */ +const waitForFleetServerToRegisterWithElasticsearch = async ( + kbnClient: KbnClient, + fleetServerHostname: string, + timeoutMs: number = 30000 +): Promise => { + const started = new Date(); + const hasTimedOut = (): boolean => { + const elapsedTime = Date.now() - started.getTime(); + return elapsedTime > timeoutMs; + }; + let found = false; + + while (!found && !hasTimedOut()) { + found = await retryOnError(async () => { + const fleetServerRecord = await kbnClient + .request({ + method: 'POST', + path: '/api/console/proxy', + query: { + path: `${FLEET_SERVER_SERVERS_INDEX}/_search`, + method: 'GET', + }, + body: { + query: { + bool: { + filter: [ + { + term: { + 'host.name': fleetServerHostname, + }, + }, + ], + }, + }, + }, + }) + .then((response) => response.data) + .catch(catchAxiosErrorFormatAndThrow); + + return (fleetServerRecord.hits.total as estypes.SearchTotalHits).value === 1; + }, RETRYABLE_TRANSIENT_ERRORS); + + if (!found) { + // sleep and check again + await new Promise((r) => setTimeout(r, 2000)); + } + } + + if (!found) { + throw new Error( + `Timed out waiting for fleet server [${fleetServerHostname}] to register with Elasticsarch` + ); + } +}; 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 db3fe4b32f1e4..2a239ef372941 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 @@ -13,6 +13,8 @@ import type { GetAgentPoliciesRequest, GetAgentPoliciesResponse, GetAgentsResponse, + GetPackagePoliciesRequest, + GetPackagePoliciesResponse, } from '@kbn/fleet-plugin/common'; import { AGENT_API_ROUTES, @@ -20,6 +22,8 @@ import { agentRouteService, AGENTS_INDEX, API_VERSIONS, + APP_API_ROUTES, + PACKAGE_POLICY_API_ROUTES, } from '@kbn/fleet-plugin/common'; import { ToolingLog } from '@kbn/tooling-log'; import type { KbnClient } from '@kbn/test'; @@ -27,12 +31,15 @@ import type { GetFleetServerHostsResponse } from '@kbn/fleet-plugin/common/types import { enrollmentAPIKeyRouteService, fleetServerHostsRoutesService, + outputRoutesService, } from '@kbn/fleet-plugin/common/services'; import type { EnrollmentAPIKey, GetAgentsRequest, GetEnrollmentAPIKeysResponse, PostAgentUnenrollResponse, + GenerateServiceTokenResponse, + GetOutputsResponse, } from '@kbn/fleet-plugin/common/types'; import nodeFetch from 'node-fetch'; import semver from 'semver'; @@ -164,26 +171,29 @@ export const waitForHostToEnroll = async ( return found; }; -/** - * Returns the URL for the default Fleet Server connected to the stack - * @param kbnClient - */ -export const fetchFleetServerUrl = async (kbnClient: KbnClient): Promise => { - const fleetServerListResponse = await kbnClient +export const fetchFleetServerHostList = async ( + kbnClient: KbnClient +): Promise => { + return kbnClient .request({ method: 'GET', path: fleetServerHostsRoutesService.getListPath(), headers: { - 'elastic-api-version': API_VERSIONS.public.v1, - }, - query: { - perPage: 100, + 'elastic-api-version': '2023-10-31', }, }) - .catch(catchAxiosErrorFormatAndThrow) - .then((response) => response.data); + .then((response) => response.data) + .catch(catchAxiosErrorFormatAndThrow); +}; + +/** + * Returns the URL for the default Fleet Server connected to the stack + * @param kbnClient + */ +export const fetchFleetServerUrl = async (kbnClient: KbnClient): Promise => { + const fleetServerListResponse = await fetchFleetServerHostList(kbnClient); - // TODO:PT need to also pull in the Proxies and use that instead if defiend for url + // TODO:PT need to also pull in the Proxies and use that instead if defined for url? let url: string | undefined; @@ -246,8 +256,30 @@ export const fetchAgentPolicyList = async ( }, query: options, }) - .catch(catchAxiosErrorFormatAndThrow) - .then((response) => response.data); + .then((response) => response.data) + .catch(catchAxiosErrorFormatAndThrow); +}; + +/** + * Retrieves a list of Fleet Integration policies + * @param kbnClient + * @param options + */ +export const fetchIntegrationPolicyList = async ( + kbnClient: KbnClient, + options: GetPackagePoliciesRequest['query'] = {} +): Promise => { + return kbnClient + .request({ + method: 'GET', + path: PACKAGE_POLICY_API_ROUTES.LIST_PATTERN, + headers: { + 'elastic-api-version': '2023-10-31', + }, + query: options, + }) + .then((response) => response.data) + .catch(catchAxiosErrorFormatAndThrow); }; /** @@ -417,3 +449,52 @@ export const unEnrollFleetAgent = async ( return data; }; + +export const generateFleetServiceToken = async ( + kbnClient: KbnClient, + logger: ToolingLog +): Promise => { + logger.info(`Generating new Fleet Service Token`); + + const serviceToken: string = await kbnClient + .request({ + method: 'POST', + path: APP_API_ROUTES.GENERATE_SERVICE_TOKEN_PATTERN, + headers: { 'elastic-api-version': '2023-10-31' }, + body: {}, + }) + .then((response) => response.data.value) + .catch(catchAxiosErrorFormatAndThrow); + + logger.verbose(`New service token created: ${serviceToken}`); + + return serviceToken; +}; + +export const fetchFleetOutputs = async (kbnClient: KbnClient): Promise => { + return kbnClient + .request({ + method: 'GET', + path: outputRoutesService.getListPath(), + headers: { 'elastic-api-version': '2023-10-31' }, + }) + .then((response) => response.data) + .catch(catchAxiosErrorFormatAndThrow); +}; + +export const getFleetElasticsearchOutputHost = async (kbnClient: KbnClient): Promise => { + const outputs = await fetchFleetOutputs(kbnClient); + let host: string = ''; + + for (const output of outputs.items) { + if (output.type === 'elasticsearch') { + host = output?.hosts?.[0] ?? ''; + } + } + + if (!host) { + throw new Error(`An output for Elasticsearch was not found in Fleet settings`); + } + + return host; +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/network_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/network_services.ts index 9f75ee6d8a6ae..c21f93b6eba2d 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/network_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/network_services.ts @@ -7,7 +7,7 @@ import { networkInterfaces } from 'node:os'; -export const getBridgeNetworkHostIp = (): string => { +export const getLocalhostRealIp = (): string => { // reverse to get the last interface first for (const netInterfaceList of Object.values(networkInterfaces()).reverse()) { if (netInterfaceList) { diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/roles_users/endpoint_operations_analyst.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/roles_users/endpoint_operations_analyst.ts index afc8941041128..3b6f3a5c90424 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/roles_users/endpoint_operations_analyst.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/roles_users/endpoint_operations_analyst.ts @@ -6,32 +6,75 @@ */ import type { Role } from '@kbn/security-plugin/common'; -import { getNoResponseActionsRole } from './without_response_actions_role'; export const getEndpointOperationsAnalyst: () => Omit = () => { - const noResponseActionsRole = getNoResponseActionsRole(); + // IMPORTANT + // This role is sync'ed with the role used for serverless and should not be changed + // unless the role for serverless has also been changed. + // This role is the default login for cypress tests as well (defend workloads team) return { - ...noResponseActionsRole, + elasticsearch: { + cluster: [], + indices: [ + { + names: [ + 'metrics-endpoint.metadata_current_*', + '.fleet-agents*', + '.fleet-actions*', + 'apm-*-transaction*', + 'traces-apm*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + '.lists*', + '.items*', + ], + privileges: ['read'], + }, + { + names: [ + 'names:', + '.alerts-security*', + '.siem-signals-*', + '.preview.alerts-security*', + '.internal.preview.alerts-security*', + ], + privileges: ['read', 'write'], + }, + ], + run_as: [], + }, kibana: [ { - ...noResponseActionsRole.kibana[0], + base: [], feature: { - ...noResponseActionsRole.kibana[0].feature, + ml: ['read'], + actions: ['all'], + fleet: ['all'], + fleetv2: ['all'], + osquery: ['all'], + securitySolutionCases: ['all'], + builtinAlerts: ['all'], siem: [ - 'minimal_all', - + 'all', + 'read_alerts', 'policy_management_all', - + 'endpoint_list_all', 'trusted_applications_all', 'event_filters_all', 'host_isolation_exceptions_all', 'blocklist_all', - 'host_isolation_all', 'process_operations_all', 'actions_log_management_all', + 'file_operations_all', + 'execute_operations_all', ], }, + spaces: ['*'], }, ], }; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts index 39dc4ffb06896..8bc4ca071cf6a 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts @@ -18,7 +18,7 @@ import fs from 'fs'; import { CA_CERT_PATH } from '@kbn/dev-utils'; import { catchAxiosErrorFormatAndThrow } from './format_axios_error'; import { isLocalhost } from './is_localhost'; -import { getBridgeNetworkHostIp } from './network_services'; +import { getLocalhostRealIp } from './network_services'; import { createSecuritySuperuser } from './security_user_services'; const CA_CERTIFICATE: Buffer = fs.readFileSync(CA_CERT_PATH); @@ -116,18 +116,16 @@ export const createRuntimeServices = async ({ let password = _password; if (asSuperuser) { - await waitForKibana( - createKbnClient({ log, url: kibanaUrl, username, password, apiKey, noCertForSsl }) - ); - const tmpEsClient = createEsClient({ - url: elasticsearchUrl, + const tmpKbnClient = createKbnClient({ + url: kibanaUrl, username, password, - log, noCertForSsl, + log, }); - const isServerlessEs = (await tmpEsClient.info()).version.build_flavor === 'serverless'; + await waitForKibana(tmpKbnClient); + const isServerlessEs = await isServerlessKibanaFlavor(tmpKbnClient); if (isServerlessEs) { log?.warning( @@ -138,7 +136,15 @@ export const createRuntimeServices = async ({ username = 'system_indices_superuser'; password = 'changeme'; } else { - const superuserResponse = await createSecuritySuperuser(tmpEsClient); + const superuserResponse = await createSecuritySuperuser( + createEsClient({ + url: elasticsearchUrl, + username: esUsername ?? username, + password: esPassword ?? password, + log, + noCertForSsl, + }) + ); ({ username, password } = superuserResponse); @@ -163,7 +169,7 @@ export const createRuntimeServices = async ({ noCertForSsl, }), log, - localhostRealIp: getBridgeNetworkHostIp(), + localhostRealIp: getLocalhostRealIp(), apiKey: apiKey ?? '', user: { username, @@ -296,13 +302,7 @@ export const fetchStackVersion = async (kbnClient: KbnClient): Promise = }; export const fetchKibanaStatus = async (kbnClient: KbnClient): Promise => { - return kbnClient - .request({ - method: 'GET', - path: '/api/status', - }) - .catch(catchAxiosErrorFormatAndThrow) - .then((response) => response.data); + return (await kbnClient.status.get().catch(catchAxiosErrorFormatAndThrow)) as StatusResponse; }; /** @@ -312,10 +312,12 @@ export const fetchKibanaStatus = async (kbnClient: KbnClient): Promise => { await pRetry( async () => { - try { - await kbnClient.status.get(); - } catch (err) { - throw new Error(`Kibana not available: ${err.message}`); + const response = await kbnClient.status.get(); + + if (response.status.overall.level !== 'available') { + throw new Error( + `Kibana not available. [status.overall.level: ${response.status.overall.level}]` + ); } }, { maxTimeout: 10000 } 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 index ae2464487cb75..ec13f2f6ff1b1 100644 --- 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 @@ -20,11 +20,11 @@ import type { } 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, - API_VERSIONS, - APP_API_ROUTES, } from '@kbn/fleet-plugin/common'; import type { FleetServerHost, @@ -43,8 +43,8 @@ import type { PostFleetServerHostsResponse, } from '@kbn/fleet-plugin/common/types/rest_spec/fleet_server_hosts'; import chalk from 'chalk'; -import { resolve } from 'path'; -import { SERVERLESS_NODES, verifyDockerInstalled, maybeCreateDockerNetwork } from '@kbn/es'; +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'; @@ -53,8 +53,6 @@ import { dump } from './utils'; import { fetchFleetServerUrl, waitForHostToEnroll } from '../common/fleet_services'; import { getRuntimeServices } from './runtime'; -const FLEET_SERVER_CUSTOM_CONFIG = resolve(__dirname, './fleet_server.yml'); - export const runFleetServerIfNeeded = async (): Promise< { fleetServerContainerId: string; fleetServerAgentPolicyId: string | undefined } | undefined > => { diff --git a/x-pack/plugins/security_solution/scripts/endpoint/fleet_server/index.ts b/x-pack/plugins/security_solution/scripts/endpoint/fleet_server/index.ts new file mode 100644 index 0000000000000..5f96d946a85eb --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/fleet_server/index.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RunContext } from '@kbn/dev-cli-runner'; +import { run } from '@kbn/dev-cli-runner'; +import { createRuntimeServices } from '../common/stack_services'; +import { startFleetServer } from '../common/fleet_server/fleet_server_services'; + +export const cli = async () => { + return run( + async (cliContext: RunContext) => { + const username = cliContext.flags.username as string; + const password = cliContext.flags.password as string; + const kibanaUrl = cliContext.flags.kibanaUrl as string; + const elasticUrl = cliContext.flags.elasticUrl as string; + const version = cliContext.flags.version as string; + const policy = cliContext.flags.policy as string; + const port = cliContext.flags.port as unknown as number; + const force = cliContext.flags.force as boolean; + const log = cliContext.log; + + const { kbnClient, log: logger } = await createRuntimeServices({ + kibanaUrl, + elasticsearchUrl: elasticUrl, + username, + password, + log, + }); + + const runningServer = await startFleetServer({ + kbnClient, + logger, + policy, + port, + version, + force, + }); + + log.info(`\n\n${runningServer.info}`); + }, + { + description: 'Start fleet-server locally and connect it to Kibana/ES', + flags: { + string: ['kibanaUrl', 'elasticUrl', 'username', 'password', 'version', 'policy'], + boolean: ['force'], + default: { + kibanaUrl: 'http://127.0.0.1:5601', + elasticUrl: 'http://127.0.0.1:9200', + username: 'elastic', + password: 'changeme', + version: '', + policy: '', + force: false, + port: 8220, + }, + help: ` + --version Optional. The Agent version to be used when installing fleet server. + Default: uses the same version as the stack (kibana). Version + can also be from 'SNAPSHOT'. + NOTE: this value will be specifically set to 'latest' when ran against + kibana in serverless mode. + Examples: 8.6.0, 8.7.0-SNAPSHOT + --policy Optional. The UUID of the agent policy that should be used to enroll + fleet-server with Kibana/ES (Default: uses existing (if found) or + creates a new one) + --force Optional. If true, then fleet-server will be started and connected to + kibana even if one seems to already be configured. + --port Optional. The port number where fleet-server will listen for requests. + (Default: 8220) + --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) +`, + }, + } + ); +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/start_fleet_server.js b/x-pack/plugins/security_solution/scripts/endpoint/start_fleet_server.js new file mode 100644 index 0000000000000..d83c8a350f9e9 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/start_fleet_server.js @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +require('../../../../../src/setup_node_env'); +require('./fleet_server').cli(); diff --git a/x-pack/plugins/security_solution/scripts/run_cypress/get_ftr_config.ts b/x-pack/plugins/security_solution/scripts/run_cypress/get_ftr_config.ts index 0a620fc1715ad..cc3972cba0b2f 100644 --- a/x-pack/plugins/security_solution/scripts/run_cypress/get_ftr_config.ts +++ b/x-pack/plugins/security_solution/scripts/run_cypress/get_ftr_config.ts @@ -9,7 +9,7 @@ import _ from 'lodash'; import { EsVersion, readConfigFile } from '@kbn/test'; import type { ToolingLog } from '@kbn/tooling-log'; import { CA_TRUSTED_FINGERPRINT } from '@kbn/dev-utils'; -import { getBridgeNetworkHostIp } from '../endpoint/common/network_services'; +import { getLocalhostRealIp } from '../endpoint/common/network_services'; import type { parseTestFileConfig } from './utils'; export const getFTRConfig = ({ @@ -58,7 +58,7 @@ export const getFTRConfig = ({ // }, }, (vars) => { - const hostRealIp = getBridgeNetworkHostIp(); + const hostRealIp = getLocalhostRealIp(); const hasFleetServerArgs = _.some( vars.kbnTestServer.serverArgs, diff --git a/x-pack/test/defend_workflows_cypress/config.ts b/x-pack/test/defend_workflows_cypress/config.ts index fc49ad2b3d7ad..bc771a8790e38 100644 --- a/x-pack/test/defend_workflows_cypress/config.ts +++ b/x-pack/test/defend_workflows_cypress/config.ts @@ -7,7 +7,7 @@ import { FtrConfigProviderContext } from '@kbn/test'; import { CA_CERT_PATH } from '@kbn/dev-utils'; -import { getBridgeNetworkHostIp } from '@kbn/security-solution-plugin/scripts/endpoint/common/network_services'; +import { getLocalhostRealIp } from '@kbn/security-solution-plugin/scripts/endpoint/common/network_services'; import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { @@ -18,7 +18,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('../functional/config.base.js') ); - const hostIp = getBridgeNetworkHostIp(); + const hostIp = getLocalhostRealIp(); return { ...kibanaCommonTestsConfig.getAll(), diff --git a/x-pack/test/defend_workflows_cypress/serverless_config.ts b/x-pack/test/defend_workflows_cypress/serverless_config.ts index 3063ab6d91876..b3b01d69c4331 100644 --- a/x-pack/test/defend_workflows_cypress/serverless_config.ts +++ b/x-pack/test/defend_workflows_cypress/serverless_config.ts @@ -5,10 +5,11 @@ * 2.0. */ -import { getBridgeNetworkHostIp } from '@kbn/security-solution-plugin/scripts/endpoint/common/network_services'; +import { getLocalhostRealIp } from '@kbn/security-solution-plugin/scripts/endpoint/common/network_services'; import { FtrConfigProviderContext } from '@kbn/test'; import { ExperimentalFeatures } from '@kbn/security-solution-plugin/common/experimental_features'; +import { ES_RESOURCES } from '@kbn/security-solution-plugin/scripts/endpoint/common/roles_users/serverless'; import { DefendWorkflowsCypressCliTestRunner } from './runner'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { @@ -18,7 +19,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ) ); const config = defendWorkflowsCypressConfig.getAll(); - const hostIp = getBridgeNetworkHostIp(); + const hostIp = getLocalhostRealIp(); const enabledFeatureFlags: Array = []; @@ -29,7 +30,10 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...config.esTestCluster, serverArgs: [...config.esTestCluster.serverArgs, 'http.host=0.0.0.0'], }, - + esServerlessOptions: { + ...(config.esServerlessOptions ?? {}), + resources: Object.values(ES_RESOURCES), + }, servers: { ...config.servers, fleetserver: { diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/security_config.ts b/x-pack/test_serverless/functional/test_suites/security/cypress/security_config.ts index ad5267de51bb5..e9b8a16c0b9c7 100644 --- a/x-pack/test_serverless/functional/test_suites/security/cypress/security_config.ts +++ b/x-pack/test_serverless/functional/test_suites/security/cypress/security_config.ts @@ -7,6 +7,7 @@ import { FtrConfigProviderContext } from '@kbn/test'; +import { ES_RESOURCES } from '@kbn/security-solution-plugin/scripts/endpoint/common/roles_users/serverless'; import type { FtrProviderContext } from './runner'; import { SecuritySolutionCypressTestRunner } from './runner'; @@ -18,6 +19,13 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { return { ...securitySolutionCypressConfig.getAll(), + esServerlessOptions: { + ...(securitySolutionCypressConfig.has('esServerlessOptions') + ? securitySolutionCypressConfig.get('esServerlessOptions') ?? {} + : {}), + resources: Object.values(ES_RESOURCES), + }, + testRunner: (context: FtrProviderContext) => SecuritySolutionCypressTestRunner(context), }; }