Setup E2E against Serverless ES, Kibana, Fleet server standalone and Elastic endpoint agent in VM (#167720)

## Summary

Run Defend Workflows Cypress E2E against Serverless stack, similar to
https://github.com/elastic/kibana/pull/165415

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Gloria Hornero <gloria.hornero@elastic.co>
This commit is contained in:
Patryk Kopyciński 2023-10-04 09:56:08 +00:00 committed by GitHub
parent 7e24f2512d
commit 5fb9a3889e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 281 additions and 281 deletions

View file

@ -27,8 +27,7 @@ disabled:
- x-pack/test/functional_enterprise_search/cypress.config.ts
- x-pack/test/defend_workflows_cypress/cli_config.ts
- x-pack/test/defend_workflows_cypress/config.ts
- x-pack/test/defend_workflows_cypress/endpoint_config.ts
- x-pack/test/defend_workflows_cypress/endpoint_serverless_config.ts
- x-pack/test/defend_workflows_cypress/serverless_config.ts
- x-pack/plugins/observability_onboarding/e2e/ftr_config_open.ts
- x-pack/plugins/observability_onboarding/e2e/ftr_config_runner.ts
- x-pack/plugins/observability_onboarding/e2e/ftr_config.ts

View file

@ -35,6 +35,7 @@ export const serverless: Command = {
--basePath Path to the directory where the ES cluster will store data
--clean Remove existing file system object store before running
--kill Kill running ES serverless nodes if detected on startup
--host Publish ES docker container on additional host IP
--port The port to bind to on 127.0.0.1 [default: ${DEFAULT_PORT}]
--ssl Enable HTTP SSL on the ES cluster
--skipTeardown If this process exits, leave the ES cluster running in the background
@ -72,7 +73,7 @@ export const serverless: Command = {
files: 'F',
},
string: ['tag', 'image', 'basePath', 'resources'],
string: ['tag', 'image', 'basePath', 'resources', 'host'],
boolean: ['clean', 'ssl', 'kill', 'background', 'skipTeardown', 'waitForReady'],
default: defaults,

View file

@ -29,6 +29,7 @@ import {
teardownServerlessClusterSync,
verifyDockerInstalled,
getESp12Volume,
ServerlessOptions,
} from './docker';
import { ToolingLog, ToolingLogCollectingWriter } from '@kbn/tooling-log';
import { ES_P12_PATH } from '@kbn/dev-utils';
@ -155,6 +156,19 @@ describe('resolvePort()', () => {
`);
});
test('should return default port when custom host passed in options', () => {
const port = resolvePort({ host: '192.168.25.1' } as ServerlessOptions);
expect(port).toMatchInlineSnapshot(`
Array [
"-p",
"127.0.0.1:9200:9200",
"-p",
"192.168.25.1:9200:9200",
]
`);
});
test('should return custom port when passed in options', () => {
const port = resolvePort({ port: 9220 });
@ -167,6 +181,21 @@ describe('resolvePort()', () => {
]
`);
});
test('should return custom port and host when passed in options', () => {
const port = resolvePort({ port: 9220, host: '192.168.25.1' });
expect(port).toMatchInlineSnapshot(`
Array [
"-p",
"127.0.0.1:9220:9220",
"-p",
"192.168.25.1:9220:9220",
"--env",
"http.port=9220",
]
`);
});
});
describe('verifyDockerInstalled()', () => {

View file

@ -56,6 +56,8 @@ export interface DockerOptions extends EsClusterExecOptions, BaseOptions {
}
export interface ServerlessOptions extends EsClusterExecOptions, BaseOptions {
/** Publish ES docker container on additional host IP */
host?: string;
/** Clean (or delete) all data created by the ES cluster after it is stopped */
clean?: boolean;
/** Path to the directory where the ES cluster will store data */
@ -306,19 +308,21 @@ export function resolveDockerImage({
}
/**
* Determine the port to bind the Serverless index node or Docker node to
* Determine the port and optionally an additional host to bind the Serverless index node or Docker node to
*/
export function resolvePort(options: ServerlessOptions | DockerOptions) {
if (options.port) {
return [
'-p',
`127.0.0.1:${options.port}:${options.port}`,
'--env',
`http.port=${options.port}`,
];
const port = options.port || DEFAULT_PORT;
const value = ['-p', `127.0.0.1:${port}:${port}`];
if ((options as ServerlessOptions).host) {
value.push('-p', `${(options as ServerlessOptions).host}:${port}:${port}`);
}
return ['-p', `127.0.0.1:${DEFAULT_PORT}:${DEFAULT_PORT}`];
if (options.port) {
value.push('--env', `http.port=${options.port}`);
}
return value;
}
/**

View file

@ -19,6 +19,7 @@ import { Client, HttpConnection } from '@elastic/elasticsearch';
import type { ToolingLog } from '@kbn/tooling-log';
import { REPO_ROOT } from '@kbn/repo-info';
import type { ArtifactLicense } from '@kbn/es';
import type { ServerlessOptions } from '@kbn/es/src/utils';
import { CI_PARALLEL_PROCESS_PREFIX } from '../ci_parallel_process_prefix';
import { esTestConfig } from './es_test_config';
@ -70,10 +71,7 @@ export interface CreateTestEsClusterOptions {
*/
esArgs?: string[];
esFrom?: string;
esServerlessOptions?: {
image?: string;
tag?: string;
};
esServerlessOptions?: Pick<ServerlessOptions, 'image' | 'tag' | 'host'>;
esJavaOpts?: string;
/**
* License to run your cluster under. Keep in mind that a `trial` license
@ -246,6 +244,7 @@ export function createTestEsCluster<
esArgs: customEsArgs,
image: esServerlessOptions?.image,
tag: esServerlessOptions?.tag,
host: esServerlessOptions?.host,
port,
clean: true,
background: true,

View file

@ -214,6 +214,12 @@ export const schema = Joi.object()
})
.default(),
esServerlessOptions: Joi.object()
.keys({
host: Joi.string().ip(),
})
.default(),
kbnTestServer: Joi.object()
.keys({
buildArgs: Joi.array(),

View file

@ -166,16 +166,24 @@ function getESServerlessOptions(esServerlessImageFromArg: string | undefined, co
esTestConfig.getESServerlessImage() ||
(config.has('esTestCluster.esServerlessImage') &&
config.get('esTestCluster.esServerlessImage'));
const serverlessHost: string | undefined =
config.has('esServerlessOptions.host') && config.get('esServerlessOptions.host');
if (esServerlessImageUrlOrTag) {
if (esServerlessImageUrlOrTag.includes(':')) {
return {
image: esServerlessImageUrlOrTag,
host: serverlessHost,
};
} else {
return {
tag: esServerlessImageUrlOrTag,
host: serverlessHost,
};
}
}
return {
host: serverlessHost,
};
}

View file

@ -13,7 +13,7 @@
"cypress:dw": "NODE_OPTIONS=--openssl-legacy-provider node ./scripts/start_cypress_parallel --config-file ./public/management/cypress/cypress.config.ts --ftr-config-file ../../test/defend_workflows_cypress/cli_config",
"cypress:dw:open": "yarn cypress:dw open",
"cypress:dw:run": "yarn cypress:dw run",
"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 ../../../x-pack/test_serverless/functional/test_suites/security/cypress/security_config",
"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:endpoint": "echo '\n** WARNING **: Run script `cypress:dw:endpoint` no longer valid! Use `cypress:dw` instead\n'",

View file

@ -63,16 +63,8 @@ export const getCypressBaseConfig = (
experimentalInteractiveRunEvents: true,
setupNodeEvents: (on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions) => {
dataLoaders(on, config);
// skip dataLoadersForRealEndpoints() if running in serverless
// https://github.com/elastic/security-team/issues/7467
// Once we are able to run Fleet server in serverless mode (see: https://github.com/elastic/kibana/pull/166183)
// this `if()` statement needs to be removed and `dataLoadersForRealEndpoints()` should
// just be called without having any checks around it.
if (!config.env.IS_SERVERLESS) {
// Data loaders specific to "real" Endpoint testing
dataLoadersForRealEndpoints(on, config);
}
// Data loaders specific to "real" Endpoint testing
dataLoadersForRealEndpoints(on, config);
responseActionTasks(on, config);

View file

@ -30,7 +30,7 @@ const loginWithoutAccess = (url: string) => {
loadPage(url);
};
describe('Artifacts pages', { tags: '@ess' }, () => {
describe('Artifacts pages', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => {
before(() => {
login();
loadEndpointDataForEventFiltersIfNeeded();

View file

@ -18,7 +18,7 @@ import { cleanupRule, generateRandomStringName, loadRule } from '../../tasks/api
import { RESPONSE_ACTION_TYPES } from '../../../../../common/api/detection_engine';
import { login, ROLE } from '../../tasks/login';
describe('Form', { tags: '@ess' }, () => {
describe('Form', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => {
describe('User with no access can not create an endpoint response action', () => {
before(() => {
login(ROLE.endpoint_response_actions_no_access);

View file

@ -12,79 +12,83 @@ import { indexEndpointRuleAlerts } from '../../tasks/index_endpoint_rule_alerts'
import { login, ROLE } from '../../tasks/login';
describe('Response actions history page', { tags: '@ess' }, () => {
let endpointData: ReturnTypeFromChainable<typeof indexEndpointHosts> | undefined;
let endpointDataWithAutomated: ReturnTypeFromChainable<typeof indexEndpointHosts> | undefined;
let alertData: ReturnTypeFromChainable<typeof indexEndpointRuleAlerts> | undefined;
const [endpointAgentId, endpointHostname] = generateRandomStringName(2);
describe(
'Response actions history page',
{ tags: ['@ess', '@serverless', '@brokenInServerless'] },
() => {
let endpointData: ReturnTypeFromChainable<typeof indexEndpointHosts> | undefined;
let endpointDataWithAutomated: ReturnTypeFromChainable<typeof indexEndpointHosts> | undefined;
let alertData: ReturnTypeFromChainable<typeof indexEndpointRuleAlerts> | undefined;
const [endpointAgentId, endpointHostname] = generateRandomStringName(2);
before(() => {
login(ROLE.endpoint_response_actions_access);
before(() => {
login(ROLE.endpoint_response_actions_access);
indexEndpointHosts({ numResponseActions: 2 }).then((indexEndpoints) => {
endpointData = indexEndpoints;
});
indexEndpointRuleAlerts({
endpointAgentId,
endpointHostname,
endpointIsolated: false,
}).then((indexedAlert) => {
alertData = indexedAlert;
const alertId = alertData.alerts[0]._id;
return indexEndpointHosts({
numResponseActions: 1,
alertIds: [alertId],
}).then((indexEndpoints) => {
endpointDataWithAutomated = indexEndpoints;
indexEndpointHosts({ numResponseActions: 2 }).then((indexEndpoints) => {
endpointData = indexEndpoints;
});
indexEndpointRuleAlerts({
endpointAgentId,
endpointHostname,
endpointIsolated: false,
}).then((indexedAlert) => {
alertData = indexedAlert;
const alertId = alertData.alerts[0]._id;
return indexEndpointHosts({
numResponseActions: 1,
alertIds: [alertId],
}).then((indexEndpoints) => {
endpointDataWithAutomated = indexEndpoints;
});
});
});
});
after(() => {
if (endpointDataWithAutomated) {
endpointDataWithAutomated.cleanup();
endpointDataWithAutomated = undefined;
}
if (endpointData) {
endpointData.cleanup();
endpointData = undefined;
}
after(() => {
if (endpointDataWithAutomated) {
endpointDataWithAutomated.cleanup();
endpointDataWithAutomated = undefined;
}
if (endpointData) {
endpointData.cleanup();
endpointData = undefined;
}
if (alertData) {
alertData.cleanup();
alertData = undefined;
}
});
it('enable filtering by type', () => {
cy.visit(`/app/security/administration/response_actions_history`);
let maxLength: number;
cy.getByTestSubj('response-actions-list').then(($table) => {
maxLength = $table.find('tbody .euiTableRow').length;
cy.get('tbody .euiTableRow').should('have.lengthOf', maxLength);
if (alertData) {
alertData.cleanup();
alertData = undefined;
}
});
cy.getByTestSubj('response-actions-list-type-filter-popoverButton').click();
cy.getByTestSubj('type-filter-option').contains('Triggered by rule').click();
cy.getByTestSubj('response-actions-list').within(() => {
cy.get('tbody .euiTableRow').should('have.lengthOf', 1);
cy.get('tbody .euiTableRow').eq(0).contains('Triggered by rule');
it('enable filtering by type', () => {
cy.visit(`/app/security/administration/response_actions_history`);
let maxLength: number;
cy.getByTestSubj('response-actions-list').then(($table) => {
maxLength = $table.find('tbody .euiTableRow').length;
cy.get('tbody .euiTableRow').should('have.lengthOf', maxLength);
});
cy.getByTestSubj('response-actions-list-type-filter-popoverButton').click();
cy.getByTestSubj('type-filter-option').contains('Triggered by rule').click();
cy.getByTestSubj('response-actions-list').within(() => {
cy.get('tbody .euiTableRow').should('have.lengthOf', 1);
cy.get('tbody .euiTableRow').eq(0).contains('Triggered by rule');
});
cy.getByTestSubj('type-filter-option').contains('Triggered by rule').click();
cy.getByTestSubj('response-actions-list').within(() => {
cy.get('tbody .euiTableRow').should('have.lengthOf', maxLength);
});
cy.getByTestSubj('type-filter-option').contains('Triggered manually').click();
cy.getByTestSubj('response-actions-list').within(() => {
cy.get('tbody .euiTableRow').should('have.lengthOf', maxLength - 1);
});
cy.getByTestSubj('type-filter-option').contains('Triggered by rule').click();
cy.getByTestSubj('response-actions-list').within(() => {
cy.get('tbody .euiTableRow').should('have.lengthOf', maxLength);
cy.get('tbody .euiTableRow').eq(0).contains('Triggered by rule').click();
});
// check if we were moved to Rules app after clicking Triggered by rule
cy.getByTestSubj('breadcrumb last').contains('Detection rules (SIEM)');
});
cy.getByTestSubj('type-filter-option').contains('Triggered by rule').click();
cy.getByTestSubj('response-actions-list').within(() => {
cy.get('tbody .euiTableRow').should('have.lengthOf', maxLength);
});
cy.getByTestSubj('type-filter-option').contains('Triggered manually').click();
cy.getByTestSubj('response-actions-list').within(() => {
cy.get('tbody .euiTableRow').should('have.lengthOf', maxLength - 1);
});
cy.getByTestSubj('type-filter-option').contains('Triggered by rule').click();
cy.getByTestSubj('response-actions-list').within(() => {
cy.get('tbody .euiTableRow').should('have.lengthOf', maxLength);
cy.get('tbody .euiTableRow').eq(0).contains('Triggered by rule').click();
});
// check if we were moved to Rules app after clicking Triggered by rule
cy.getByTestSubj('breadcrumb last').contains('Detection rules (SIEM)');
});
});
}
);

View file

@ -15,7 +15,7 @@ import { indexEndpointRuleAlerts } from '../../tasks/index_endpoint_rule_alerts'
import { login, ROLE } from '../../tasks/login';
describe('Results', { tags: '@ess' }, () => {
describe('Results', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => {
let endpointData: ReturnTypeFromChainable<typeof indexEndpointHosts> | undefined;
let alertData: ReturnTypeFromChainable<typeof indexEndpointRuleAlerts> | undefined;
const [endpointAgentId, endpointHostname] = generateRandomStringName(2);

View file

@ -32,7 +32,7 @@ import { createEndpointHost } from '../../tasks/create_endpoint_host';
import { deleteAllLoadedEndpointData } from '../../tasks/delete_all_endpoint_data';
import { enableAllPolicyProtections } from '../../tasks/endpoint_policy';
describe('Endpoints page', { tags: '@ess' }, () => {
describe('Endpoints page', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => {
let indexedPolicy: IndexedFleetEndpointPolicyResponse;
let policy: PolicyData;
let createdHost: CreateAndEnrollEndpointHostResponse;

View file

@ -13,7 +13,7 @@ import { indexEndpointHosts } from '../../tasks/index_endpoint_hosts';
import { login } from '../../tasks/login';
import { loadPage } from '../../tasks/common';
describe('Endpoints page', { tags: '@ess' }, () => {
describe('Endpoints page', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => {
let endpointData: ReturnTypeFromChainable<typeof indexEndpointHosts>;
before(() => {

View file

@ -15,7 +15,7 @@ import { navigateToFleetAgentDetails } from '../../screens/fleet/agent_details';
import { EndpointPolicyResponseGenerator } from '../../../../../common/endpoint/data_generators/endpoint_policy_response_generator';
import { descriptions } from '../../../components/policy_response/policy_response_friendly_names';
describe.skip('Endpoint Policy Response', { tags: '@ess' }, () => {
describe.skip('Endpoint Policy Response', { tags: ['@ess', '@serverless'] }, () => {
let loadedEndpoint: CyIndexEndpointHosts;
let endpointMetadata: HostMetadata;
let loadedPolicyResponse: IndexedEndpointPolicyResponse;

View file

@ -19,7 +19,8 @@ import { disableExpandableFlyoutAdvancedSettings, loadPage } from '../../tasks/c
describe(
'Policy Details',
{
tags: '@ess',
tags: ['@ess', '@serverless', '@brokenInServerless'],
env: { ftrConfig: { enableExperimental: ['protectionUpdatesEnabled'] } },
},
() => {
describe('Protection updates', () => {

View file

@ -13,7 +13,7 @@ import { login } from '../../tasks/login';
import { createAgentPolicyTask, getEndpointIntegrationVersion } from '../../tasks/fleet';
// We need a way to disable experimental features in the Cypress tests
describe.skip('Disabled experimental features on: ', { tags: '@ess' }, () => {
describe.skip('Disabled experimental features on: ', { tags: ['@ess', '@serverless'] }, () => {
describe('Policy list', () => {
describe('Renders policy list without protection updates feature flag', () => {
let indexedPolicy: IndexedFleetEndpointPolicyResponse;

View file

@ -16,7 +16,7 @@ import { createAgentPolicyTask, getEndpointIntegrationVersion } from '../../task
describe(
'Policy List',
{
tags: '@ess',
tags: ['@ess', '@serverless', '@brokenInServerless'],
env: { ftrConfig: { enableExperimental: ['protectionUpdatesEnabled'] } },
},
() => {

View file

@ -29,7 +29,7 @@ import { indexNewCase } from '../../tasks/index_new_case';
import { indexEndpointHosts } from '../../tasks/index_endpoint_hosts';
import { indexEndpointRuleAlerts } from '../../tasks/index_endpoint_rule_alerts';
describe('Isolate command', { tags: '@ess' }, () => {
describe('Isolate command', { tags: ['@ess', '@serverless'] }, () => {
describe('from Manage', () => {
let endpointData: ReturnTypeFromChainable<typeof indexEndpointHosts> | undefined;
let isolatedEndpointData: ReturnTypeFromChainable<typeof indexEndpointHosts> | undefined;
@ -192,7 +192,7 @@ describe('Isolate command', { tags: '@ess' }, () => {
});
});
describe('from Cases', () => {
describe('from Cases', { tags: ['@brokenInServerless'] }, () => {
let endpointData: ReturnTypeFromChainable<typeof indexEndpointHosts> | undefined;
let caseData: ReturnTypeFromChainable<typeof indexNewCase> | undefined;
let alertData: ReturnTypeFromChainable<typeof indexEndpointRuleAlerts> | undefined;

View file

@ -10,65 +10,69 @@ import { indexEndpointHosts } from '../../tasks/index_endpoint_hosts';
import { login } from '../../tasks/login';
import { loadPage } from '../../tasks/common';
describe('Response actions history page', { tags: '@ess' }, () => {
let endpointData: ReturnTypeFromChainable<typeof indexEndpointHosts>;
// let actionData: ReturnTypeFromChainable<typeof indexActionResponses>;
describe(
'Response actions history page',
{ tags: ['@ess', '@serverless', '@brokenInServerless'] },
() => {
let endpointData: ReturnTypeFromChainable<typeof indexEndpointHosts>;
// let actionData: ReturnTypeFromChainable<typeof indexActionResponses>;
before(() => {
indexEndpointHosts({ numResponseActions: 11 }).then((indexEndpoints) => {
endpointData = indexEndpoints;
before(() => {
indexEndpointHosts({ numResponseActions: 11 }).then((indexEndpoints) => {
endpointData = indexEndpoints;
});
});
});
beforeEach(() => {
login();
});
beforeEach(() => {
login();
});
after(() => {
if (endpointData) {
endpointData.cleanup();
// @ts-expect-error ignore setting to undefined
endpointData = undefined;
}
});
after(() => {
if (endpointData) {
endpointData.cleanup();
// @ts-expect-error ignore setting to undefined
endpointData = undefined;
}
});
it('retains expanded action details on page reload', () => {
loadPage(`/app/security/administration/response_actions_history`);
cy.getByTestSubj('response-actions-list-expand-button').eq(3).click(); // 4th row on 1st page
cy.getByTestSubj('response-actions-list-details-tray').should('exist');
cy.url().should('include', 'withOutputs');
it('retains expanded action details on page reload', () => {
loadPage(`/app/security/administration/response_actions_history`);
cy.getByTestSubj('response-actions-list-expand-button').eq(3).click(); // 4th row on 1st page
cy.getByTestSubj('response-actions-list-details-tray').should('exist');
cy.url().should('include', 'withOutputs');
// navigate to page 2
cy.getByTestSubj('pagination-button-1').click();
cy.getByTestSubj('response-actions-list-details-tray').should('not.exist');
// reload with URL params on page 2 with existing URL
cy.reload();
cy.getByTestSubj('response-actions-list-details-tray').should('not.exist');
// navigate to page 1
cy.getByTestSubj('pagination-button-0').click();
cy.getByTestSubj('response-actions-list-details-tray').should('exist');
});
it('collapses expanded tray with a single click', () => {
loadPage(`/app/security/administration/response_actions_history`);
// 2nd row on 1st page
cy.getByTestSubj('response-actions-list-expand-button').eq(1).as('2nd-row');
// expand the row
cy.get('@2nd-row').click();
cy.getByTestSubj('response-actions-list-details-tray').should('exist');
cy.url().should('include', 'withOutputs');
// collapse the row
cy.intercept('GET', '/api/endpoint/action*').as('getResponses');
cy.get('@2nd-row').click();
// wait for the API response to come back
// and then see if the tray is actually closed
cy.wait('@getResponses', { timeout: 500 }).then(() => {
// navigate to page 2
cy.getByTestSubj('pagination-button-1').click();
cy.getByTestSubj('response-actions-list-details-tray').should('not.exist');
cy.url().should('not.include', 'withOutputs');
// reload with URL params on page 2 with existing URL
cy.reload();
cy.getByTestSubj('response-actions-list-details-tray').should('not.exist');
// navigate to page 1
cy.getByTestSubj('pagination-button-0').click();
cy.getByTestSubj('response-actions-list-details-tray').should('exist');
});
});
});
it('collapses expanded tray with a single click', () => {
loadPage(`/app/security/administration/response_actions_history`);
// 2nd row on 1st page
cy.getByTestSubj('response-actions-list-expand-button').eq(1).as('2nd-row');
// expand the row
cy.get('@2nd-row').click();
cy.getByTestSubj('response-actions-list-details-tray').should('exist');
cy.url().should('include', 'withOutputs');
// collapse the row
cy.intercept('GET', '/api/endpoint/action*').as('getResponses');
cy.get('@2nd-row').click();
// wait for the API response to come back
// and then see if the tray is actually closed
cy.wait('@getResponses', { timeout: 500 }).then(() => {
cy.getByTestSubj('response-actions-list-details-tray').should('not.exist');
cy.url().should('not.include', 'withOutputs');
});
});
}
);

View file

@ -21,7 +21,7 @@ import { indexNewCase } from '../../tasks/index_new_case';
import { indexEndpointHosts } from '../../tasks/index_endpoint_hosts';
import { indexEndpointRuleAlerts } from '../../tasks/index_endpoint_rule_alerts';
describe('When accessing Endpoint Response Console', { tags: '@ess' }, () => {
describe('When accessing Endpoint Response Console', { tags: ['@ess', '@serverless'] }, () => {
const performResponderSanityChecks = () => {
openResponderActionLogFlyout();
// Ensure the popover in the action log date quick select picker is accessible
@ -109,12 +109,16 @@ describe('When accessing Endpoint Response Console', { tags: '@ess' }, () => {
cy.getByTestSubj('endpointResponseActions-action-item').should('be.enabled');
});
it('should display Responder response action interface', () => {
loadPage(caseUrlPath);
closeAllToasts();
openCaseAlertDetails();
cy.getByTestSubj('endpointResponseActions-action-item').click();
performResponderSanityChecks();
});
it(
'should display Responder response action interface',
{ tags: ['@brokenInServerless'] },
() => {
loadPage(caseUrlPath);
closeAllToasts();
openCaseAlertDetails();
cy.getByTestSubj('endpointResponseActions-action-item').click();
performResponderSanityChecks();
}
);
});
});

View file

@ -24,12 +24,12 @@ import {
} from '../../tasks/isolate';
import { login } from '../../tasks/login';
describe('Response console', { tags: '@ess' }, () => {
describe('Response console', { tags: ['@ess', '@serverless'] }, () => {
beforeEach(() => {
login();
});
describe('`isolate` command', () => {
describe('`isolate` command', { tags: ['@brokenInServerless'] }, () => {
let endpointData: ReturnTypeFromChainable<typeof indexEndpointHosts>;
let endpointHostname: string;
let isolateRequestResponse: ActionDetails;
@ -71,7 +71,7 @@ describe('Response console', { tags: '@ess' }, () => {
});
});
describe('`release` command', () => {
describe('`release` command', { tags: ['@brokenInServerless'] }, () => {
let endpointData: ReturnTypeFromChainable<typeof indexEndpointHosts>;
let endpointHostname: string;
let releaseRequestResponse: ActionDetails;

View file

@ -7,8 +7,9 @@
import { networkInterfaces } from 'node:os';
export const getLocalhostRealIp = (): string => {
for (const netInterfaceList of Object.values(networkInterfaces())) {
export const getBridgeNetworkHostIp = (): string => {
// reverse to get the last interface first
for (const netInterfaceList of Object.values(networkInterfaces()).reverse()) {
if (netInterfaceList) {
const netInterface = netInterfaceList.find(
(networkInterface) =>

View file

@ -11,7 +11,6 @@ import type { KbnClientOptions } from '@kbn/test';
import { KbnClient } from '@kbn/test';
import type { StatusResponse } from '@kbn/core-status-common-internal';
import pRetry from 'p-retry';
import nodeFetch from 'node-fetch';
import type { ReqOptions } from '@kbn/test/src/kbn_client/kbn_client_requester';
import { type AxiosResponse } from 'axios';
import type { ClientOptions } from '@elastic/elasticsearch/lib/client';
@ -19,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 { getLocalhostRealIp } from './localhost_services';
import { getBridgeNetworkHostIp } from './network_services';
import { createSecuritySuperuser } from './security_user_services';
const CA_CERTIFICATE: Buffer = fs.readFileSync(CA_CERT_PATH);
@ -117,7 +116,9 @@ export const createRuntimeServices = async ({
let password = _password;
if (asSuperuser) {
await waitForKibana(kibanaUrl);
await waitForKibana(
createKbnClient({ log, url: kibanaUrl, username, password, apiKey, noCertForSsl })
);
const tmpEsClient = createEsClient({
url: elasticsearchUrl,
username,
@ -162,7 +163,7 @@ export const createRuntimeServices = async ({
noCertForSsl,
}),
log,
localhostRealIp: getLocalhostRealIp(),
localhostRealIp: getBridgeNetworkHostIp(),
apiKey: apiKey ?? '',
user: {
username,
@ -306,24 +307,15 @@ export const fetchKibanaStatus = async (kbnClient: KbnClient): Promise<StatusRes
/**
* Checks to ensure Kibana is up and running
* @param kbnUrl
* @param kbnClient
*/
export const waitForKibana = async (kbnUrl: string): Promise<void> => {
const url = (() => {
const u = new URL(kbnUrl);
// This API seems to be available even if user is not authenticated
u.pathname = '/api/status';
return u.toString();
})();
export const waitForKibana = async (kbnClient: KbnClient): Promise<void> => {
await pRetry(
async () => {
const response = await nodeFetch(url);
if (response.status !== 200) {
throw new Error(
`Kibana not available. Returned: [${response.status}]: ${response.statusText}`
);
try {
await kbnClient.status.get();
} catch (err) {
throw new Error(`Kibana not available: ${err.message}`);
}
},
{ maxTimeout: 10000 }

View file

@ -6,11 +6,10 @@
*/
import _ from 'lodash';
import { SERVERLESS_NODES } from '@kbn/es';
import { EsVersion, readConfigFile } from '@kbn/test';
import type { ToolingLog } from '@kbn/tooling-log';
import { CA_TRUSTED_FINGERPRINT } from '@kbn/dev-utils';
import { getLocalhostRealIp } from '../endpoint/common/localhost_services';
import { getBridgeNetworkHostIp } from '../endpoint/common/network_services';
import type { parseTestFileConfig } from './utils';
export const getFTRConfig = ({
@ -59,7 +58,7 @@ export const getFTRConfig = ({
// },
},
(vars) => {
const hostRealIp = getLocalhostRealIp();
const hostRealIp = getBridgeNetworkHostIp();
const hasFleetServerArgs = _.some(
vars.kbnTestServer.serverArgs,
@ -90,15 +89,15 @@ export const getFTRConfig = ({
vars.kbnTestServer.serverArgs = _.map(vars.kbnTestServer.serverArgs, (value) => {
if (
vars.servers.elasticsearch.protocol === 'https' &&
value.includes('--elasticsearch.hosts=http')
value.includes('--elasticsearch.hosts=http://')
) {
return value.replace('http', 'https');
}
if (
vars.servers.kibana.protocol === 'https' &&
(value.includes('--elasticsearch.hosts=http') ||
value.includes('--server.publicBaseUrl=http'))
(value.includes('--elasticsearch.hosts=http://') ||
value.includes('--server.publicBaseUrl=http://'))
) {
return value.replace('http', 'https');
}
@ -135,14 +134,32 @@ export const getFTRConfig = ({
if (hasFleetServerArgs) {
if (vars.serverless) {
vars.esServerlessOptions = {
...(vars.esServerlessOptions || {}),
// Bind ES docker container to the host network so that the Elastic agent running in the VM can connect to it
host: hostRealIp,
};
vars.kbnTestServer.serverArgs.push(
`--xpack.fleet.agents.fleet_server.hosts=["https://host.docker.internal:${fleetServerPort}"]`
`--xpack.fleet.agents.fleet_server.hosts=["https://${hostRealIp}:${fleetServerPort}"]`
);
vars.kbnTestServer.serverArgs.push(
`--xpack.fleet.agents.elasticsearch.host=https://${SERVERLESS_NODES[0].name}:${esPort}`
);
vars.kbnTestServer.serverArgs.push(
`--xpack.fleet.agents.elasticsearch.ca_trusted_fingerprint=${CA_TRUSTED_FINGERPRINT}`
`--xpack.fleet.outputs=${JSON.stringify([
{
id: 'fleet-default-output',
name: 'default',
is_default: true,
is_default_monitoring: true,
type: 'elasticsearch',
ca_trusted_fingerprint: CA_TRUSTED_FINGERPRINT,
hosts: [`https://${hostRealIp}:${esPort}`],
config: {
ssl: {
verification_mode: 'none',
},
},
},
])}`
);
} else {
vars.kbnTestServer.serverArgs.push(
@ -158,20 +175,11 @@ export const getFTRConfig = ({
if (vars.serverless) {
log.info(`Serverless mode detected`);
vars.kbnTestServer.serverArgs.push(
`--elasticsearch.hosts=https://localhost:${esPort}`,
`--server.publicBaseUrl=https://localhost:${kibanaPort}`
);
vars.esTestCluster.serverArgs.push(
`xpack.security.authc.realms.saml.cloud-saml-kibana.sp.entity_id=http://host.docker.internal:${kibanaPort}`,
`xpack.security.authc.realms.saml.cloud-saml-kibana.sp.logout=http://host.docker.internal:${kibanaPort}/logout`,
`xpack.security.authc.realms.saml.cloud-saml-kibana.sp.acs=http://host.docker.internal:${kibanaPort}/api/security/saml/callback`
);
} else {
vars.kbnTestServer.serverArgs.push(
`--elasticsearch.hosts=http://localhost:${esPort}`,
`--server.publicBaseUrl=http://localhost:${kibanaPort}`
);
}
if (specFileFTRConfig?.productTypes) {

View file

@ -176,6 +176,10 @@ ${JSON.stringify(cypressConfigFile, null, 2)}
const fleetServerPorts: number[] = [8220];
const getEsPort = <T>(): T | number => {
if (isOpen) {
return 9220;
}
const esPort = parseInt(`92${Math.floor(Math.random() * 89) + 10}`, 10);
if (esPorts.includes(esPort)) {
return getEsPort();

View file

@ -7,7 +7,7 @@
import { FtrConfigProviderContext } from '@kbn/test';
import { CA_CERT_PATH } from '@kbn/dev-utils';
import { getLocalhostRealIp } from '@kbn/security-solution-plugin/scripts/endpoint/common/localhost_services';
import { getBridgeNetworkHostIp } 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 = getLocalhostRealIp();
const hostIp = getBridgeNetworkHostIp();
return {
...kibanaCommonTestsConfig.getAll(),

View file

@ -1,65 +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 { getLocalhostRealIp } from '@kbn/security-solution-plugin/scripts/endpoint/common/localhost_services';
import { FtrConfigProviderContext } from '@kbn/test';
import { ExperimentalFeatures } from '@kbn/security-solution-plugin/common/experimental_features';
import { DefendWorkflowsCypressCliTestRunner } from './runner';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const defendWorkflowsCypressConfig = await readConfigFile(require.resolve('./config.ts'));
const svlSharedConfig = await readConfigFile(
require.resolve('../../test_serverless/shared/config.base.ts')
);
const hostIp = getLocalhostRealIp();
const enabledFeatureFlags: Array<keyof ExperimentalFeatures> = [];
return {
...svlSharedConfig.getAll(),
esTestCluster: {
...svlSharedConfig.get('esTestCluster'),
serverArgs: [
...svlSharedConfig.get('esTestCluster.serverArgs'),
// define custom es server here
// API Keys is enabled at the top level
],
},
servers: {
...svlSharedConfig.get('servers'),
fleetserver: {
protocol: 'https',
hostname: hostIp,
port: 8220,
},
},
kbnTestServer: {
...svlSharedConfig.get('kbnTestServer'),
serverArgs: [
...svlSharedConfig.get('kbnTestServer.serverArgs'),
'--csp.strict=false',
'--csp.warnLegacyBrowsers=false',
'--serverless=security',
'--xpack.encryptedSavedObjects.encryptionKey="abcdefghijklmnopqrstuvwxyz123456"',
'--xpack.security.enabled=true',
`--xpack.fleet.agents.fleet_server.hosts=["https://${hostIp}:8220"]`,
`--xpack.fleet.agents.elasticsearch.host=http://${hostIp}:${defendWorkflowsCypressConfig.get(
'servers.elasticsearch.port'
)}`,
// set the packagerTaskInterval to 5s in order to speed up test executions when checking fleet artifacts
'--xpack.securitySolution.packagerTaskInterval=5s',
`--xpack.securitySolution.enableExperimental=${JSON.stringify(enabledFeatureFlags)}`,
],
},
testRunner: DefendWorkflowsCypressCliTestRunner,
};
}

View file

@ -5,22 +5,31 @@
* 2.0.
*/
import { getLocalhostRealIp } from '@kbn/security-solution-plugin/scripts/endpoint/common/localhost_services';
import { getBridgeNetworkHostIp } 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 { DefendWorkflowsCypressCliTestRunner } from './runner';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const defendWorkflowsCypressConfig = await readConfigFile(require.resolve('./config.ts'));
const defendWorkflowsCypressConfig = await readConfigFile(
require.resolve(
'../../test_serverless/functional/test_suites/security/cypress/security_config.base.ts'
)
);
const config = defendWorkflowsCypressConfig.getAll();
const hostIp = getLocalhostRealIp();
const hostIp = getBridgeNetworkHostIp();
const enabledFeatureFlags: Array<keyof ExperimentalFeatures> = [];
return {
...config,
esTestCluster: {
...config.esTestCluster,
serverArgs: [...config.esTestCluster.serverArgs, 'http.host=0.0.0.0'],
},
servers: {
...config.servers,
fleetserver: {