diff --git a/.buildkite/pipelines/security_solution/security_solution_defend_workflows.yml b/.buildkite/pipelines/security_solution/security_solution_defend_workflows.yml new file mode 100644 index 0000000000000..3eb6b216d17db --- /dev/null +++ b/.buildkite/pipelines/security_solution/security_solution_defend_workflows.yml @@ -0,0 +1,11 @@ +steps: + - command: .buildkite/scripts/pipelines/security_solution_quality_gate/edr_workflows/mki_security_solution_defend_workflows.sh + label: 'Defend Workflows Cypress Tests on Serverless' + agents: + queue: n2-4-virt + timeout_in_minutes: 300 + parallelism: 6 + retry: + automatic: + - exit_status: '*' + limit: 1 \ No newline at end of file diff --git a/.buildkite/scripts/pipelines/security_solution_quality_gate/edr_workflows/mki_security_solution_defend_workflows.sh b/.buildkite/scripts/pipelines/security_solution_quality_gate/edr_workflows/mki_security_solution_defend_workflows.sh new file mode 100755 index 0000000000000..5bc8627c3acae --- /dev/null +++ b/.buildkite/scripts/pipelines/security_solution_quality_gate/edr_workflows/mki_security_solution_defend_workflows.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -euo pipefail + +source .buildkite/scripts/common/util.sh +source .buildkite/scripts/steps/functional/common_cypress.sh +.buildkite/scripts/bootstrap.sh + +export JOB=kibana-defend-workflows-serverless-cypress + +cd x-pack/plugins/security_solution +set +e + +QA_API_KEY=$(retry 5 5 vault read -field=qa_api_key secret/kibana-issues/dev/security-solution-qg-enc-key) + +CLOUD_QA_API_KEY=$QA_API_KEY yarn cypress:dw:qa:serverless:run; status=$?; yarn junit:merge || :; exit $status \ No newline at end of file diff --git a/.buildkite/scripts/pipelines/security_solution_quality_gate/edr_workflows/pipeline.sh b/.buildkite/scripts/pipelines/security_solution_quality_gate/edr_workflows/pipeline.sh index 807ec48ab48ed..375ed9c1f847e 100755 --- a/.buildkite/scripts/pipelines/security_solution_quality_gate/edr_workflows/pipeline.sh +++ b/.buildkite/scripts/pipelines/security_solution_quality_gate/edr_workflows/pipeline.sh @@ -2,4 +2,4 @@ set -euo pipefail -echo "Running the EDR-Workflows testing for Kibana" \ No newline at end of file +ts-node .buildkite/scripts/pipelines/security_solution_quality_gate/edr_workflows/pipeline.ts diff --git a/.buildkite/scripts/pipelines/security_solution_quality_gate/edr_workflows/pipeline.ts b/.buildkite/scripts/pipelines/security_solution_quality_gate/edr_workflows/pipeline.ts new file mode 100644 index 0000000000000..c8b59df2de305 --- /dev/null +++ b/.buildkite/scripts/pipelines/security_solution_quality_gate/edr_workflows/pipeline.ts @@ -0,0 +1,43 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { execSync } from 'child_process'; +import fs from 'fs'; + +const getPipeline = (filename: string, removeSteps = true) => { + const str = fs.readFileSync(filename).toString(); + return removeSteps ? str.replace(/^steps:/, '') : str; +}; + +const uploadPipeline = (pipelineContent: string | object) => { + const str = + typeof pipelineContent === 'string' ? pipelineContent : JSON.stringify(pipelineContent); + + execSync('buildkite-agent pipeline upload', { + input: str, + stdio: ['pipe', 'inherit', 'inherit'], + }); +}; + +(async () => { + try { + const pipeline = []; + + pipeline.push( + getPipeline( + '.buildkite/pipelines/security_solution/security_solution_defend_workflows.yml', + false + ) + ); + // remove duplicated steps + uploadPipeline([...new Set(pipeline)].join('\n')); + } catch (ex) { + console.error('PR pipeline generation error', ex.message); + process.exit(1); + } +})(); diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index a13046b0bc59f..c982997a673dd 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -16,6 +16,8 @@ "cypress:dw:serverless": "NODE_OPTIONS=--openssl-legacy-provider node ./scripts/start_cypress_parallel --config-file ./public/management/cypress/cypress_serverless.config.ts --ftr-config-file ../../test/defend_workflows_cypress/serverless_config", "cypress:dw:serverless:open": "yarn cypress:dw:serverless open", "cypress:dw:serverless:run": "yarn cypress:dw:serverless run", + "cypress:dw:mki:serverless": "NODE_OPTIONS=--openssl-legacy-provider node ./scripts/start_cypress_parallel_serverless --config-file ./public/management/cypress/cypress_serverless_qa.config.ts", + "cypress:dw:qa:serverless:run": "yarn cypress:dw:mki:serverless run", "cypress:dw:serverless:changed-specs-only": "yarn cypress:dw:serverless run --changed-specs-only --env burn=2", "cypress:dw:endpoint": "echo '\n** WARNING **: Run script `cypress:dw:endpoint` no longer valid! Use `cypress:dw` instead\n'", "cypress:dw:endpoint:run": "echo '\n** WARNING **: Run script `cypress:dw:endpoint:run` no longer valid! Use `cypress:dw:run` instead\n'", diff --git a/x-pack/plugins/security_solution/public/management/cypress/README.md b/x-pack/plugins/security_solution/public/management/cypress/README.md index 79689e650faea..18df10f351840 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/README.md +++ b/x-pack/plugins/security_solution/public/management/cypress/README.md @@ -34,6 +34,7 @@ for more information. Similarly to Security Solution cypress tests, we use tags in order to select which tests we want to execute on which environment: - `@serverless` includes a test in the Serverless test suite. You need to explicitly add this tag to any test you want to run against a Serverless environment. +- `'@cloudServerless` includes the test in the Serverless test suite that executes in a Cloud environment (MKI). - `@ess` includes a test in the normal, non-Serverless test suite. You need to explicitly add this tag to any test you want to run against a non-Serverless environment. - `@brokenInServerless` excludes a test from the Serverless test suite (even if it's tagged as `@serverless`). Indicates that a test should run in Serverless, but currently is broken. - `@skipInServerless` excludes a test from the Serverless test suite (even if it's tagged as `@serverless`). Indicates that we don't want to run the given test in Serverless. 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 6ed65f031d714..c611592a1429d 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 @@ -74,6 +74,7 @@ export const getCypressBaseConfig = ( experimentalRunAllSpecs: true, experimentalMemoryManagement: true, experimentalInteractiveRunEvents: true, + experimentalCspAllowList: ['default-src', 'script-src', 'script-src-elem'], setupNodeEvents: async (on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions) => { // IMPORTANT: setting the log level should happen before any tooling is called setupToolingLogLevel(config); diff --git a/x-pack/plugins/security_solution/public/management/cypress/cypress_serverless_qa.config.ts b/x-pack/plugins/security_solution/public/management/cypress/cypress_serverless_qa.config.ts new file mode 100644 index 0000000000000..ea86cc56078d8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/cypress_serverless_qa.config.ts @@ -0,0 +1,23 @@ +/* + * 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 { defineCypressConfig } from '@kbn/cypress-config'; +import { getCypressBaseConfig } from './cypress_base.config'; + +// eslint-disable-next-line import/no-default-export +export default defineCypressConfig( + getCypressBaseConfig({ + env: { + IS_SERVERLESS: true, + + // Uncomment to enable logging + // TOOLING_LOG_LEVEL: 'verbose', + + grepTags: '@cloudServerless --@brokenInServerless', + }, + }) +); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console/process_operations.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console/process_operations.cy.ts index c7120ded692b9..e8ced44446dc2 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console/process_operations.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/response_console/process_operations.cy.ts @@ -24,7 +24,7 @@ import { enableAllPolicyProtections } from '../../../tasks/endpoint_policy'; import { createEndpointHost } from '../../../tasks/create_endpoint_host'; import { deleteAllLoadedEndpointData } from '../../../tasks/delete_all_endpoint_data'; -describe('Response console', { tags: ['@ess', '@serverless'] }, () => { +describe('Response console', { tags: ['@ess', '@serverless', '@cloudServerless'] }, () => { beforeEach(() => { login(); }); 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 index c356536cc03d4..2364f08b39681 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/support/common.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/support/common.ts @@ -25,7 +25,7 @@ export const setupStackServicesUsingCypressConfig = async (config: Cypress.Plugi password: config.env.KIBANA_PASSWORD, esUsername: config.env.ELASTICSEARCH_USERNAME, esPassword: config.env.ELASTICSEARCH_PASSWORD, - asSuperuser: true, + asSuperuser: !config.env.CLOUD_SERVERLESS, }).then(({ log, ...others }) => { return { ...others, diff --git a/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts b/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts index 99ea877053c91..c0f939193fd53 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 @@ -127,6 +127,7 @@ export const dataLoaders = ( ): void => { // Env. variable is set by `cypress_serverless.config.ts` const isServerless = config.env.IS_SERVERLESS; + const isCloudServerless = Boolean(config.env.CLOUD_SERVERLESS); const stackServicesPromise = setupStackServicesUsingCypressConfig(config); const roleAndUserLoaderPromise: Promise = stackServicesPromise.then( ({ kbnClient, log }) => { @@ -259,8 +260,8 @@ export const dataLoaders = ( }: { endpointAgentIds: string[]; }): Promise => { - const { esClient } = await stackServicesPromise; - return deleteAllEndpointData(esClient, endpointAgentIds); + const { esClient, log } = await stackServicesPromise; + return deleteAllEndpointData(esClient, log, endpointAgentIds, !isCloudServerless); }, /** 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 38abf64ce202e..3e44d64f960cb 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 @@ -101,7 +101,7 @@ Cypress.Commands.add( Cypress.on('uncaught:exception', () => false); -// Login as a SOC_MANAGER to properly initialize Security Solution App +// Before any tests runs, Login and visit the Alerts page so that it properly initializes the Security Solution App before(() => { login(ROLE.soc_manager); loadPage('/app/security/alerts'); diff --git a/x-pack/plugins/security_solution/public/management/cypress/support/plugin_handlers/endpoint_data_loader.ts b/x-pack/plugins/security_solution/public/management/cypress/support/plugin_handlers/endpoint_data_loader.ts index b81a446a09a7d..90d3dd572c6a4 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/support/plugin_handlers/endpoint_data_loader.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/support/plugin_handlers/endpoint_data_loader.ts @@ -10,6 +10,7 @@ import type { KbnClient } from '@kbn/test'; import pRetry from 'p-retry'; import { kibanaPackageJson } from '@kbn/repo-info'; import type { ToolingLog } from '@kbn/tooling-log'; +import { dump } from '../../../../../scripts/endpoint/common/utils'; import { STARTED_TRANSFORM_STATES } from '../../../../../common/constants'; import { ENDPOINT_ALERTS_INDEX, @@ -79,8 +80,8 @@ export const cyLoadEndpointDataHandler = async ( if (waitUntilTransformed) { // need this before indexing docs so that the united transform doesn't // create a checkpoint with a timestamp after the doc timestamps - await stopTransform(esClient, metadataTransformPrefix); - await stopTransform(esClient, METADATA_UNITED_TRANSFORM); + await stopTransform(esClient, log, metadataTransformPrefix); + await stopTransform(esClient, log, METADATA_UNITED_TRANSFORM); } // load data into the system @@ -120,13 +121,23 @@ export const cyLoadEndpointDataHandler = async ( return indexedData; }; -const stopTransform = async (esClient: Client, transformId: string): Promise => { - await esClient.transform.stopTransform({ - transform_id: `${transformId}*`, - force: true, - wait_for_completion: true, - allow_no_match: true, - }); +const stopTransform = async ( + esClient: Client, + log: ToolingLog, + transformId: string +): Promise => { + await esClient.transform + .stopTransform({ + transform_id: `${transformId}*`, + force: true, + wait_for_completion: true, + allow_no_match: true, + }) + .catch((e) => { + Error.captureStackTrace(e); + log.verbose(dump(e, 8)); + throw e; + }); }; const startTransform = async (esClient: Client, transformId: string): Promise => { diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/delete_all_endpoint_data.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/delete_all_endpoint_data.ts index 9877fbd906a67..5cbcc3d010ec2 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/delete_all_endpoint_data.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/delete_all_endpoint_data.ts @@ -7,7 +7,9 @@ import type { Client, estypes } from '@elastic/elasticsearch'; import assert from 'assert'; +import type { ToolingLog } from '@kbn/tooling-log'; import { createEsClient, isServerlessKibanaFlavor } from './stack_services'; +import type { CreatedSecuritySuperuser } from './security_user_services'; import { createSecuritySuperuser } from './security_user_services'; export interface DeleteAllEndpointDataResponse { @@ -22,24 +24,49 @@ export interface DeleteAllEndpointDataResponse { * **NOTE:** This utility will create a new role and user that has elevated privileges and access to system indexes. * * @param esClient + * @param log * @param endpointAgentIds + * @param asSuperuser */ export const deleteAllEndpointData = async ( esClient: Client, - endpointAgentIds: string[] + log: ToolingLog, + endpointAgentIds: string[], + /** If true, then a new user will be created that has full privileges to indexes (especially system indexes) */ + asSuperuser: boolean = true ): Promise => { assert(endpointAgentIds.length > 0, 'At least one endpoint agent id must be defined'); - const isServerless = await isServerlessKibanaFlavor(esClient); - const unrestrictedUser = isServerless - ? { password: 'changeme', username: 'system_indices_superuser', created: false } - : await createSecuritySuperuser(esClient, 'super_superuser'); - const esUrl = getEsUrlFromClient(esClient); - const esClientUnrestricted = createEsClient({ - url: esUrl, - username: unrestrictedUser.username, - password: unrestrictedUser.password, - }); + let esClientUnrestricted = esClient; + + if (asSuperuser) { + log.debug(`Looking to use a superuser type of account`); + + const isServerless = await isServerlessKibanaFlavor(esClient); + let unrestrictedUser: CreatedSecuritySuperuser | undefined; + + if (isServerless) { + log.debug(`In serverless mode. Creating new ES Client using 'system_indices_superuser'`); + + unrestrictedUser = { + password: 'changeme', + username: 'system_indices_superuser', + created: false, + }; + } else { + log.debug(`Creating new superuser account [super_superuser]`); + unrestrictedUser = await createSecuritySuperuser(esClient, 'super_superuser'); + } + + if (unrestrictedUser) { + const esUrl = getEsUrlFromClient(esClient); + esClientUnrestricted = createEsClient({ + url: esUrl, + username: unrestrictedUser.username, + password: unrestrictedUser.password, + }); + } + } const queryString = endpointAgentIds.map((id) => `(${id})`).join(' OR '); @@ -56,6 +83,8 @@ export const deleteAllEndpointData = async ( conflicts: 'proceed', }); + log.verbose(`All deleted documents:\n`, deleteResponse); + return { count: deleteResponse.deleted ?? 0, query: queryString, 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 f6590e5ef9e2e..f0fd4990a5017 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,7 +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 { captureCallingStack, dump, prefixedOutputLogger } from '../utils'; import { createToolingLogger, RETRYABLE_TRANSIENT_ERRORS, @@ -62,7 +62,6 @@ import { getFleetElasticsearchOutputHost, waitForHostToEnroll, } from '../fleet_services'; -import { dump } from '../../endpoint_agent_runner/utils'; import { getLocalhostRealIp } from '../network_services'; import { isLocalhost } from '../is_localhost'; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts index 26ca9d6474393..10337f6baf709 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 @@ -350,12 +350,25 @@ export const fetchIntegrationPolicyList = async ( * @param kbnClient */ export const getAgentVersionMatchingCurrentStack = async ( - kbnClient: KbnClient + kbnClient: KbnClient, + log: ToolingLog = createToolingLogger() ): Promise => { const kbnStatus = await fetchKibanaStatus(kbnClient); + + log.debug(`Kibana status:\n`, kbnStatus); + + if (!kbnStatus.version) { + throw new Error( + `Kibana status api response did not include 'version' information - possibly due to invalid credentials` + ); + } + const agentVersions = await axios .get('https://artifacts-api.elastic.co/v1/versions') - .then((response) => map(response.data.versions, (version) => version.split('-SNAPSHOT')[0])); + .then((response) => { + log.verbose(`Agent Version:\n`, response.data); + return map(response.data.versions, (version) => version.split('-SNAPSHOT')[0]); + }); let version = semver.maxSatisfying(agentVersions, `<=${kbnStatus.version.number}`) ?? diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/security_user_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/security_user_services.ts index f17bf7b514f21..b3ce8e220dbeb 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/security_user_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/security_user_services.ts @@ -8,11 +8,17 @@ import type { Client } from '@elastic/elasticsearch'; import { userInfo } from 'os'; +export interface CreatedSecuritySuperuser { + username: string; + password: string; + created: boolean; +} + export const createSecuritySuperuser = async ( esClient: Client, username: string = userInfo().username, password: string = 'changeme' -): Promise<{ username: string; password: string; created: boolean }> => { +): Promise => { if (!username || !password) { throw new Error(`username and password require values.`); } 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 13fc2a1a0ffba..d67d38e10d2be 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 @@ -16,6 +16,7 @@ import { type AxiosResponse } from 'axios'; import type { ClientOptions } from '@elastic/elasticsearch/lib/client'; import fs from 'fs'; import { CA_CERT_PATH } from '@kbn/dev-utils'; +import { omit } from 'lodash'; import { createToolingLogger } from '../../../common/endpoint/data_loaders/utils'; import { catchAxiosErrorFormatAndThrow } from './format_axios_error'; import { isLocalhost } from './is_localhost'; @@ -107,15 +108,16 @@ export const createRuntimeServices = async ({ username: _username, password: _password, apiKey, - esUsername, - esPassword, - log: _log, + esUsername: _esUsername, + esPassword: _esPassword, + log = createToolingLogger(), asSuperuser = false, noCertForSsl, }: CreateRuntimeServicesOptions): Promise => { - const log = _log ?? createToolingLogger(); let username = _username; let password = _password; + let esUsername = _esUsername; + let esPassword = _esPassword; if (asSuperuser) { const tmpKbnClient = createKbnClient({ @@ -131,12 +133,15 @@ export const createRuntimeServices = async ({ if (isServerlessEs) { log?.warning( - 'Creating Security Superuser is not supported in current environment. ES is running in serverless mode. ' + + 'Creating Security Superuser is not supported in current environment.\nES is running in serverless mode. ' + 'Will use username [system_indices_superuser] instead.' ); username = 'system_indices_superuser'; password = 'changeme'; + + esUsername = 'system_indices_superuser'; + esPassword = 'changeme'; } else { const superuserResponse = await createSecuritySuperuser( createEsClient({ @@ -243,7 +248,12 @@ export const createEsClient = ({ } if (log) { - log.verbose(`Creating Elasticsearch client options: ${JSON.stringify(clientOptions)}`); + log.verbose( + `Creating Elasticsearch client options: ${JSON.stringify({ + ...omit(clientOptions, 'tls'), + ...(clientOptions.tls ? { tls: { ca: [typeof clientOptions.tls.ca] } } : {}), + })}` + ); } return new Client(clientOptions); diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/utils.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/utils.ts index 3c8e227c89271..72a59743d4435 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/utils.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/utils.ts @@ -9,6 +9,7 @@ import type { ToolingLog } from '@kbn/tooling-log'; import chalk from 'chalk'; +import { inspect } from 'util'; /** * Capture and return the calling stack for the context that called this utility. @@ -61,3 +62,12 @@ export const prefixedOutputLogger = (prefix: string, log: ToolingLog): ToolingLo return proxy; }; + +/** + * Safely traverse some content (object, array, etc) and stringify it + * @param content + * @param depth + */ +export const dump = (content: any, depth: number = 5): string => { + return inspect(content, { depth }); +}; 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 e569580eb7357..42aa3610a0124 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,6 +5,7 @@ * 2.0. */ +import { dump } from '../common/utils'; import { generateVmName } from '../common/vm_services'; import { createAndEnrollEndpointHost } from '../common/endpoint_host_services'; import { @@ -12,7 +13,6 @@ import { getOrCreateDefaultAgentPolicy, } from '../common/fleet_services'; import { getRuntimeServices } from './runtime'; -import { dump } from './utils'; export const enrollEndpointHost = async (): Promise => { let vmName; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/utils.ts b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/utils.ts deleted file mode 100644 index 669d5f830c08e..0000000000000 --- a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/utils.ts +++ /dev/null @@ -1,17 +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 { inspect } from 'util'; - -/** - * Safely traverse some content (object, array, etc) and stringify it - * @param content - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const dump = (content: any): string => { - return inspect(content, { depth: 5 }); -}; diff --git a/x-pack/plugins/security_solution/scripts/run_cypress/parallel_serverless.ts b/x-pack/plugins/security_solution/scripts/run_cypress/parallel_serverless.ts index 22b51692eb33f..b9a4620869336 100644 --- a/x-pack/plugins/security_solution/scripts/run_cypress/parallel_serverless.ts +++ b/x-pack/plugins/security_solution/scripts/run_cypress/parallel_serverless.ts @@ -16,6 +16,7 @@ import cypress from 'cypress'; import grep from '@cypress/grep/src/plugin'; import crypto from 'crypto'; import fs from 'fs'; +import { exec } from 'child_process'; import { createFailError } from '@kbn/dev-cli-errors'; import axios, { AxiosError } from 'axios'; import path from 'path'; @@ -24,9 +25,11 @@ import pRetry from 'p-retry'; import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; import { INITIAL_REST_VERSION } from '@kbn/data-views-plugin/server/constants'; -import { exec } from 'child_process'; +import { catchAxiosErrorFormatAndThrow } from '../endpoint/common/format_axios_error'; +import { createToolingLogger } from '../../common/endpoint/data_loaders/utils'; import { renderSummaryTable } from './print_run'; import { parseTestFileConfig, retrieveIntegrations } from './utils'; +import { prefixedOutputLogger } from '../endpoint/common/utils'; interface ProductType { product_line: string; @@ -97,11 +100,16 @@ async function createSecurityProject( }; try { - const response = await axios.post(`${BASE_ENV_URL}/api/v1/serverless/projects/security`, body, { - headers: { - Authorization: `ApiKey ${apiKey}`, - }, - }); + const response = await axios + .post(`${BASE_ENV_URL}/api/v1/serverless/projects/security`, body, { + headers: { + Authorization: `ApiKey ${apiKey}`, + }, + }) + .catch(catchAxiosErrorFormatAndThrow); + + log.verbose('Create Security Project response:\n', response); + return { name: response.data.name, id: response.data.id, @@ -126,11 +134,13 @@ async function deleteSecurityProject( apiKey: string ): Promise { try { - await axios.delete(`${BASE_ENV_URL}/api/v1/serverless/projects/security/${projectId}`, { - headers: { - Authorization: `ApiKey ${apiKey}`, - }, - }); + await axios + .delete(`${BASE_ENV_URL}/api/v1/serverless/projects/security/${projectId}`, { + headers: { + Authorization: `ApiKey ${apiKey}`, + }, + }) + .catch(catchAxiosErrorFormatAndThrow); log.info(`Project ${projectName} was successfully deleted!`); } catch (error) { if (error instanceof AxiosError) { @@ -150,16 +160,19 @@ async function resetCredentials( log.info(`${runnerId} : Reseting credentials`); const fetchResetCredentialsStatusAttempt = async (attemptNum: number) => { - const response = await axios.post( - `${BASE_ENV_URL}/api/v1/serverless/projects/security/${projectId}/_reset-credentials`, - {}, - { - headers: { - Authorization: `ApiKey ${apiKey}`, - }, - } - ); + const response = await axios + .post( + `${BASE_ENV_URL}/api/v1/serverless/projects/security/${projectId}/_reset-credentials`, + {}, + { + headers: { + Authorization: `ApiKey ${apiKey}`, + }, + } + ) + .catch(catchAxiosErrorFormatAndThrow); log.info('Credentials have ben reset'); + log.verbose(response); return { password: response.data.password, username: response.data.username, @@ -186,16 +199,15 @@ async function resetCredentials( function waitForProjectInitialized(projectId: string, apiKey: string): Promise { const fetchProjectStatusAttempt = async (attemptNum: number) => { log.info(`Retry number ${attemptNum} to check if project is initialized.`); - const response = await axios.get( - `${BASE_ENV_URL}/api/v1/serverless/projects/security/${projectId}/status`, - { + const response = await axios + .get(`${BASE_ENV_URL}/api/v1/serverless/projects/security/${projectId}/status`, { headers: { Authorization: `ApiKey ${apiKey}`, }, - } - ); + }) + .catch(catchAxiosErrorFormatAndThrow); if (response.data.phase !== 'initialized') { - throw new Error('Project is not initialized. A retry will be triggered soon...'); + throw new Error(`Project not yet initialized [${response.data.phase}]. Retrying...`); } else { log.info('Project is initialized'); } @@ -205,7 +217,8 @@ function waitForProjectInitialized(projectId: string, apiKey: string): Promise { log.info(`Retry number ${attemptNum} to check if Elasticsearch is green.`); - const response = await axios.get(`${esUrl}/_cluster/health?wait_for_status=green&timeout=50s`, { - headers: { - Authorization: `Basic ${auth}`, - }, - }); + const response = await axios + .get(`${esUrl}/_cluster/health?wait_for_status=green&timeout=50s`, { + headers: { + Authorization: `Basic ${auth}`, + }, + }) + .catch(catchAxiosErrorFormatAndThrow); log.info(`${runnerId}: Elasticsearch is ready with status ${response.data.status}.`); }; @@ -248,11 +263,13 @@ function waitForEsStatusGreen(esUrl: string, auth: string, runnerId: string): Pr function waitForKibanaAvailable(kbUrl: string, auth: string, runnerId: string): Promise { const fetchKibanaStatusAttempt = async (attemptNum: number) => { log.info(`Retry number ${attemptNum} to check if kibana is available.`); - const response = await axios.get(`${kbUrl}/api/status`, { - headers: { - Authorization: `Basic ${auth}`, - }, - }); + const response = await axios + .get(`${kbUrl}/api/status`, { + headers: { + Authorization: `Basic ${auth}`, + }, + }) + .catch(catchAxiosErrorFormatAndThrow); if (response.data.status.overall.level !== 'available') { throw new Error(`${runnerId}: Kibana is not available. A retry will be triggered soon...`); } else { @@ -281,11 +298,13 @@ function waitForEsAccess(esUrl: string, auth: string, runnerId: string): Promise const fetchEsAccessAttempt = async (attemptNum: number) => { log.info(`Retry number ${attemptNum} to check if can be accessed.`); - await axios.get(`${esUrl}`, { - headers: { - Authorization: `Basic ${auth}`, - }, - }); + await axios + .get(`${esUrl}`, { + headers: { + Authorization: `Basic ${auth}`, + }, + }) + .catch(catchAxiosErrorFormatAndThrow); }; const retryOptions = { onFailedAttempt: (error: Error | AxiosError) => { @@ -313,9 +332,11 @@ function waitForKibanaLogin(kbUrl: string, credentials: Credentials): Promise { log.info(`Retry number ${attemptNum} to check if login can be performed.`); - axios.post(`${kbUrl}/internal/security/login`, body, { - headers: API_HEADERS, - }); + axios + .post(`${kbUrl}/internal/security/login`, body, { + headers: API_HEADERS, + }) + .catch(catchAxiosErrorFormatAndThrow); }; const retryOptions = { onFailedAttempt: (error: Error | AxiosError) => { @@ -433,6 +454,12 @@ ${JSON.stringify(argv, null, 2)} 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; + } + // eslint-disable-next-line require-atomic-updates + log = prefixedOutputLogger('cy.parallel(svl)', createToolingLogger()); + const tier: string = argv.tier; const endpointAddon: boolean = argv.endpointAddon; const cloudAddon: boolean = argv.cloudAddon; @@ -510,8 +537,10 @@ ${JSON.stringify(cypressConfigFile, null, 2)} ? getProductTypes(tier, endpointAddon, cloudAddon) : (parseTestFileConfig(filePath).productTypes as ProductType[]); + log.info(`Running spec file: ${filePath}`); + if (!API_KEY) { - log.info('API KEY to create project could not be retrieved.'); + log.error('API KEY to create project could not be retrieved.'); // eslint-disable-next-line no-process-exit return process.exit(1); } @@ -521,7 +550,7 @@ ${JSON.stringify(cypressConfigFile, null, 2)} const project = await createSecurityProject(PROJECT_NAME, API_KEY, productTypes); if (!project) { - log.info('Failed to create project.'); + log.error('Failed to create project.'); // eslint-disable-next-line no-process-exit return process.exit(1); } @@ -535,7 +564,7 @@ ${JSON.stringify(cypressConfigFile, null, 2)} const credentials = await resetCredentials(project.id, id, API_KEY); if (!credentials) { - log.info('Credentials could not be reset.'); + log.error('Credentials could not be reset.'); // eslint-disable-next-line no-process-exit return process.exit(1); } @@ -560,7 +589,7 @@ ${JSON.stringify(cypressConfigFile, null, 2)} // Normalized the set of available env vars in cypress const cyCustomEnv = { - CYPRESS_BASE_URL: project.kb_url, + BASE_URL: project.kb_url, ELASTICSEARCH_URL: project.es_url, ELASTICSEARCH_USERNAME: credentials.username, @@ -571,17 +600,16 @@ ${JSON.stringify(cypressConfigFile, null, 2)} KIBANA_PASSWORD: credentials.password, CLOUD_SERVERLESS: true, + CLOUD_QA_API_KEY: API_KEY, }; - if (process.env.DEBUG && !process.env.CI) { - log.info(` - ---------------------------------------------- - Cypress run ENV for file: ${filePath}: - ---------------------------------------------- - ${JSON.stringify(cyCustomEnv, null, 2)} - ---------------------------------------------- - `); - } + log.debug(` +---------------------------------------------- +Cypress run ENV for file: ${filePath}: +---------------------------------------------- +${JSON.stringify(cyCustomEnv, null, 2)} +---------------------------------------------- +`); if (isOpen) { await cypress.open({