[Defend Workflows] PoC of e2e endpoint testing (#148893)

This commit is contained in:
Patryk Kopyciński 2023-02-15 19:02:46 +01:00 committed by GitHub
parent 168b9fd395
commit e5895c30ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 330 additions and 38 deletions

View file

@ -26,6 +26,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/visual_config.ts
- x-pack/test/osquery_cypress/cli_config.ts
- x-pack/test/osquery_cypress/config.ts

View file

@ -16,6 +16,7 @@ import type {
DeleteAgentPolicyResponse,
PostDeletePackagePoliciesResponse,
} from '@kbn/fleet-plugin/common';
import { kibanaPackageJson } from '@kbn/repo-info';
import { AGENT_POLICY_API_ROUTES, PACKAGE_POLICY_API_ROUTES } from '@kbn/fleet-plugin/common';
import type { PolicyData } from '../types';
import { policyFactory as policyConfigFactory } from '../models/policy_config';
@ -33,7 +34,7 @@ export interface IndexedFleetEndpointPolicyResponse {
export const indexFleetEndpointPolicy = async (
kbnClient: KbnClient,
policyName: string,
endpointPackageVersion: string = '8.0.0',
endpointPackageVersion: string = kibanaPackageJson.version,
agentPolicyName?: string
): Promise<IndexedFleetEndpointPolicyResponse> => {
const response: IndexedFleetEndpointPolicyResponse = {
@ -47,6 +48,7 @@ export const indexFleetEndpointPolicy = async (
agentPolicyName || `Policy for ${policyName} (${Math.random().toString(36).substr(2, 5)})`,
description: `Policy created with endpoint data generator (${policyName})`,
namespace: 'default',
monitoring_enabled: ['logs', 'metrics'],
};
let agentPolicy: AxiosResponse<CreateAgentPolicyResponse>;
@ -86,7 +88,7 @@ export const indexFleetEndpointPolicy = async (
namespace: 'default',
package: {
name: 'endpoint',
title: 'endpoint',
title: 'Elastic Defend',
version: endpointPackageVersion,
},
};

View file

@ -26,6 +26,8 @@
"cypress:dw:open-as-ci": "node ../../../scripts/functional_tests --config ../../test/defend_workflows_cypress/visual_config.ts",
"cypress:dw:run": "yarn cypress run --config-file ./public/management/cypress.config.ts",
"cypress:dw:run-as-ci": "node ../../../scripts/functional_tests --config ../../test/defend_workflows_cypress/cli_config.ts",
"cypress:dw:endpoint:open": "yarn cypress open --config-file ./public/management/cypress_endpoint.config.ts",
"cypress:dw:endpoint:open-as-ci": "node ../../../scripts/functional_tests --config ../../test/defend_workflows_cypress/endpoint_config.ts",
"junit:merge": "../../../node_modules/.bin/mochawesome-merge ../../../target/kibana-security-solution/cypress/results/mochawesome*.json > ../../../target/kibana-security-solution/cypress/results/output.json && ../../../node_modules/.bin/marge ../../../target/kibana-security-solution/cypress/results/output.json --reportDir ../../../target/kibana-security-solution/cypress/results && mkdir -p ../../../target/junit && cp ../../../target/kibana-security-solution/cypress/results/*.xml ../../../target/junit/",
"test:generate": "node scripts/endpoint/resolver_generator"
}

View file

@ -34,7 +34,7 @@ export default defineCypressConfig({
e2e: {
baseUrl: 'http://localhost:5620',
supportFile: 'public/management/cypress/support/e2e.ts',
specPattern: 'public/management/cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
specPattern: 'public/management/cypress/e2e/mocked_data/*.cy.{js,jsx,ts,tsx}',
experimentalRunAllSpecs: true,
},
});

View file

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { login } from '../../tasks/login';
describe('Endpoints page', () => {
beforeEach(() => {
login();
});
it('Loads the endpoints page', () => {
cy.visit('/app/security/administration/endpoints');
cy.contains('Hosts running Elastic Defend').should('exist');
});
});

View file

@ -5,18 +5,18 @@
* 2.0.
*/
import { getEndpointSecurityPolicyManager } from '../../../../scripts/endpoint/common/roles_users/endpoint_security_policy_manager';
import { getArtifactsListTestsData } from '../fixtures/artifacts_page';
import { getEndpointSecurityPolicyManager } from '../../../../../scripts/endpoint/common/roles_users/endpoint_security_policy_manager';
import { getArtifactsListTestsData } from '../../fixtures/artifacts_page';
import {
createPerPolicyArtifact,
createArtifactList,
removeAllArtifacts,
removeExceptionsList,
yieldFirstPolicyID,
} from '../tasks/artifacts';
import { loadEndpointDataForEventFiltersIfNeeded } from '../tasks/load_endpoint_data';
import { login, loginWithCustomRole, loginWithRole, ROLE } from '../tasks/login';
import { performUserActions } from '../tasks/perform_user_actions';
} from '../../tasks/artifacts';
import { loadEndpointDataForEventFiltersIfNeeded } from '../../tasks/load_endpoint_data';
import { login, loginWithCustomRole, loginWithRole, ROLE } from '../../tasks/login';
import { performUserActions } from '../../tasks/perform_user_actions';
const loginWithPrivilegeAll = () => {
loginWithRole(ROLE.endpoint_security_policy_manager);

View file

@ -5,12 +5,12 @@
* 2.0.
*/
import { login, loginWithRole, ROLE } from '../tasks/login';
import { login, loginWithRole, ROLE } from '../../tasks/login';
import { getArtifactsListTestsData } from '../fixtures/artifacts_page';
import { removeAllArtifacts } from '../tasks/artifacts';
import { performUserActions } from '../tasks/perform_user_actions';
import { loadEndpointDataForEventFiltersIfNeeded } from '../tasks/load_endpoint_data';
import { getArtifactsListTestsData } from '../../fixtures/artifacts_page';
import { removeAllArtifacts } from '../../tasks/artifacts';
import { performUserActions } from '../../tasks/perform_user_actions';
import { loadEndpointDataForEventFiltersIfNeeded } from '../../tasks/load_endpoint_data';
const loginWithWriteAccess = (url: string) => {
loginWithRole(ROLE.analyst_hunter);

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import { login } from '../tasks/login';
import { runEndpointLoaderScript } from '../tasks/run_endpoint_loader';
import { login } from '../../tasks/login';
import { runEndpointLoaderScript } from '../../tasks/run_endpoint_loader';
describe('Endpoints page', () => {
before(() => {

View file

@ -0,0 +1,41 @@
/*
* 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';
// eslint-disable-next-line import/no-default-export
export default defineCypressConfig({
defaultCommandTimeout: 60000,
execTimeout: 120000,
pageLoadTimeout: 12000,
retries: {
runMode: 1,
openMode: 0,
},
screenshotsFolder:
'../../../target/kibana-security-solution/public/management/cypress/screenshots',
trashAssetsBeforeRuns: false,
video: false,
viewportHeight: 900,
viewportWidth: 1440,
experimentalStudio: true,
env: {
'cypress-react-selector': {
root: '#security-solution-app',
},
},
e2e: {
baseUrl: 'http://localhost:5620',
supportFile: 'public/management/cypress/support/e2e.ts',
specPattern: 'public/management/cypress/e2e/endpoint/*.cy.{js,jsx,ts,tsx}',
experimentalRunAllSpecs: true,
},
});

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { pick } from 'lodash';
import type { Client, estypes } from '@elastic/elasticsearch';
import type {
Agent,
@ -14,7 +15,6 @@ import type {
GetAgentsResponse,
} from '@kbn/fleet-plugin/common';
import { AGENT_API_ROUTES, agentPolicyRouteService, AGENTS_INDEX } from '@kbn/fleet-plugin/common';
import { pick } from 'lodash';
import { ToolingLog } from '@kbn/tooling-log';
import type { KbnClient } from '@kbn/test';
import type { GetFleetServerHostsResponse } from '@kbn/fleet-plugin/common/types/rest_spec/fleet_server_hosts';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import execa from 'execa';
import { networkInterfaces } from 'node:os';
const POSSIBLE_LOCALHOST_VALUES: readonly string[] = [
'localhost',
@ -15,13 +15,21 @@ const POSSIBLE_LOCALHOST_VALUES: readonly string[] = [
'0000:0000:0000:0000:0000:0000:0000:0000',
];
export const getLocalhostRealIp = async (): Promise<string> => {
// TODO:PT find better way to get host machine public IP. Command below is not x-platform
return execa.commandSync(
"ipconfig getifaddr `scutil --dns |awk -F'[()]' '$1~/if_index/ {print $2;exit;}'`",
{ shell: true }
).stdout;
export const getLocalhostRealIp = (): string => {
for (const netInterfaceList of Object.values(networkInterfaces())) {
if (netInterfaceList) {
const netInterface = netInterfaceList.find(
(networkInterface) =>
networkInterface.family === 'IPv4' &&
networkInterface.internal === false &&
networkInterface.address
);
if (netInterface) {
return netInterface.address;
}
}
}
return '0.0.0.0';
};
export const isLocalhost = (hostname: string): boolean => {

View file

@ -44,7 +44,8 @@ interface ElasticArtifactSearchResponse {
};
}
export const enrollEndpointHost = async () => {
export const enrollEndpointHost = async (): Promise<string | undefined> => {
let vmName;
const {
log,
kbnClient,
@ -80,7 +81,7 @@ export const enrollEndpointHost = async () => {
throw new Error(`No API enrollment key found for policy id [${policyId}]`);
}
const vmName = `${username}-dev-${uniqueId}`;
vmName = `${username}-dev-${uniqueId}`;
log.info(`Creating VM named: ${vmName}`);
@ -166,6 +167,8 @@ export const enrollEndpointHost = async () => {
}
log.indent(-4);
return vmName;
};
const getAgentDownloadUrl = async (version: string): Promise<string> => {

View file

@ -44,7 +44,8 @@ import {
} from '../common/fleet_services';
import { getRuntimeServices } from './runtime';
export const runFleetServerIfNeeded = async () => {
export const runFleetServerIfNeeded = async (): Promise<string | undefined> => {
let fleetServerContainerId;
const {
log,
kibana: { isLocalhost: isKibanaOnLocalhost },
@ -69,7 +70,7 @@ export const runFleetServerIfNeeded = async () => {
await configureFleetIfNeeded();
}
await startFleetServerWithDocker({
fleetServerContainerId = await startFleetServerWithDocker({
policyId: fleetServerAgentPolicyId,
serviceToken,
});
@ -80,6 +81,8 @@ export const runFleetServerIfNeeded = async () => {
}
log.indent(-4);
return fleetServerContainerId;
};
const isFleetServerEnrolled = async () => {
@ -178,13 +181,14 @@ const generateFleetServiceToken = async (): Promise<string> => {
return serviceToken;
};
const startFleetServerWithDocker = async ({
export const startFleetServerWithDocker = async ({
policyId,
serviceToken,
}: {
policyId: string;
serviceToken: string;
}) => {
let containerId;
const {
log,
localhostRealIp,
@ -256,16 +260,16 @@ const startFleetServerWithDocker = async ({
(This is ok if one was not running already)`);
});
await addFleetServerHostToFleetSettings(`https://${localhostRealIp}:8220`);
log.verbose(`docker arguments:\n${dockerArgs.join(' ')}`);
const containerId = (await execa('docker', dockerArgs)).stdout;
containerId = (await execa('docker', dockerArgs)).stdout;
const fleetServerAgent = await waitForHostToEnroll(kbnClient, containerName);
log.verbose(`Fleet server enrolled agent:\n${JSON.stringify(fleetServerAgent, null, 2)}`);
await addFleetServerHostToFleetSettings(`https://${localhostRealIp}:8220`);
log.info(`Done. Fleet Server is running and connected to Fleet.
Container Name: ${containerName}
Container Id: ${containerId}
@ -280,6 +284,8 @@ const startFleetServerWithDocker = async ({
}
log.indent(-4);
return containerId;
};
const configureFleetIfNeeded = async () => {

View file

@ -12,8 +12,9 @@ import type { RuntimeServices } from '../common/stack_services';
import { createRuntimeServices } from '../common/stack_services';
interface EndpointRunnerRuntimeServices extends RuntimeServices {
options: Required<
Omit<StartRuntimeServicesOptions, 'kibanaUrl' | 'elasticUrl' | 'username' | 'password' | 'log'>
options: Omit<
StartRuntimeServicesOptions,
'kibanaUrl' | 'elasticUrl' | 'username' | 'password' | 'log'
>;
}

View file

@ -12,7 +12,7 @@ export interface StartRuntimeServicesOptions {
elasticUrl: string;
username: string;
password: string;
version: string;
policy: string;
version?: string;
policy?: string;
log?: ToolingLog;
}

View file

@ -0,0 +1,35 @@
/*
* 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 execa from 'execa';
import { ToolingLog } from '@kbn/tooling-log';
import { enrollEndpointHost } from '@kbn/security-solution-plugin/scripts/endpoint/endpoint_agent_runner/elastic_endpoint';
import { Manager } from './resource_manager';
export class AgentManager extends Manager {
private log: ToolingLog;
private vmName?: string;
constructor(log: ToolingLog) {
super();
this.log = log;
this.vmName = undefined;
}
public async setup() {
this.vmName = await enrollEndpointHost();
}
public cleanup() {
super.cleanup();
this.log.info('Cleaning up the agent process');
if (this.vmName) {
execa.commandSync(`multipass delete -p ${this.vmName}`);
this.log.info('Agent process closed');
}
}
}

View file

@ -7,6 +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 { services } from './services';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
@ -17,6 +18,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
require.resolve('../functional/config.base.js')
);
const hostIp = getLocalhostRealIp();
return {
...kibanaCommonTestsConfig.getAll(),
@ -41,6 +44,10 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
'--csp.strict=false',
// define custom kibana server args here
`--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`,
`--xpack.fleet.agents.fleet_server.hosts=["https://${hostIp}:8220"]`,
`--xpack.fleet.agents.elasticsearch.host=http://${hostIp}:${kibanaCommonTestsConfig.get(
'servers.elasticsearch.port'
)}`,
// always install Endpoint package by default when Fleet sets up
`--xpack.fleet.packages.0.name=endpoint`,
`--xpack.fleet.packages.0.version=latest`,

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { getLocalhostRealIp } from '@kbn/security-solution-plugin/scripts/endpoint/common/localhost_services';
import { FtrConfigProviderContext } from '@kbn/test';
import { DefendWorkflowsCypressEndpointTestRunner } from './runner';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const defendWorkflowsCypressConfig = await readConfigFile(require.resolve('./config.ts'));
const config = defendWorkflowsCypressConfig.getAll();
const hostIp = getLocalhostRealIp();
return {
...config,
kbnTestServer: {
...config.kbnTestServer,
serverArgs: [
...config.kbnTestServer.serverArgs,
`--xpack.fleet.agents.fleet_server.hosts=["https://${hostIp}:8220"]`,
`--xpack.fleet.agents.elasticsearch.host=http://${hostIp}:${defendWorkflowsCypressConfig.get(
'servers.elasticsearch.port'
)}`,
],
},
testRunner: DefendWorkflowsCypressEndpointTestRunner,
};
}

View file

@ -0,0 +1,37 @@
/*
* 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 execa from 'execa';
import { ToolingLog } from '@kbn/tooling-log';
import { runFleetServerIfNeeded } from '@kbn/security-solution-plugin/scripts/endpoint/endpoint_agent_runner/fleet_server';
import { Manager } from './resource_manager';
export class FleetManager extends Manager {
private fleetContainerId?: string;
private log: ToolingLog;
constructor(log: ToolingLog) {
super();
this.log = log;
}
public async setup(): Promise<void> {
this.fleetContainerId = await runFleetServerIfNeeded();
}
public cleanup() {
super.cleanup();
this.log.info('Removing old fleet config');
if (this.fleetContainerId) {
this.log.info('Closing fleet process');
execa.sync('docker', ['kill', this.fleetContainerId]);
this.log.info('Fleet server process closed');
}
}
}

View file

@ -0,0 +1,15 @@
/*
* 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.
*/
const CLEANUP_EVENTS = ['SIGINT', 'exit', 'uncaughtException', 'unhandledRejection'];
export class Manager {
constructor() {
const cleanup = () => this.cleanup();
CLEANUP_EVENTS.forEach((ev) => process.on(ev, cleanup));
}
cleanup() {}
}

View file

@ -8,8 +8,46 @@
import { resolve } from 'path';
import Url from 'url';
import { withProcRunner } from '@kbn/dev-proc-runner';
import { startRuntimeServices } from '@kbn/security-solution-plugin/scripts/endpoint/endpoint_agent_runner/runtime';
import { FtrProviderContext } from './ftr_provider_context';
import { AgentManager } from './agent';
import { FleetManager } from './fleet_server';
import { getLatestAvailableAgentVersion } from './utils';
async function withFleetAgent(
{ getService }: FtrProviderContext,
runner: (runnerEnv: Record<string, string>) => Promise<void>
) {
const log = getService('log');
const config = getService('config');
const kbnClient = getService('kibanaServer');
const elasticUrl = Url.format(config.get('servers.elasticsearch'));
const kibanaUrl = Url.format(config.get('servers.kibana'));
const username = config.get('servers.elasticsearch.username');
const password = config.get('servers.elasticsearch.password');
await startRuntimeServices({
log,
elasticUrl,
kibanaUrl,
username,
password,
version: await getLatestAvailableAgentVersion(kbnClient),
});
const fleetManager = new FleetManager(log);
const agentManager = new AgentManager(log);
await fleetManager.setup();
await agentManager.setup();
try {
await runner({});
} finally {
agentManager.cleanup();
fleetManager.cleanup();
}
}
export async function DefendWorkflowsCypressCliTestRunner(context: FtrProviderContext) {
await startDefendWorkflowsCypress(context, 'dw:run');
@ -19,6 +57,10 @@ export async function DefendWorkflowsCypressVisualTestRunner(context: FtrProvide
await startDefendWorkflowsCypress(context, 'dw:open');
}
export async function DefendWorkflowsCypressEndpointTestRunner(context: FtrProviderContext) {
await withFleetAgent(context, () => startDefendWorkflowsCypress(context, 'dw:endpoint:open'));
}
function startDefendWorkflowsCypress(context: FtrProviderContext, cypressCommand: string) {
const log = context.getService('log');
const config = context.getService('config');

View file

@ -0,0 +1,41 @@
/*
* 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 axios from 'axios';
import semver from 'semver';
import { filter } from 'lodash';
import { KbnClient } from '@kbn/test';
/**
* Returns the Agent version that is available for install (will check `artifacts-api.elastic.co/v1/versions`)
* that is equal to or less than `maxVersion`.
* @param maxVersion
*/
export const getLatestAvailableAgentVersion = async (kbnClient: KbnClient): Promise<string> => {
const kbnStatus = await kbnClient.status.get();
const agentVersions = await axios
.get('https://artifacts-api.elastic.co/v1/versions')
.then((response) =>
filter(response.data.versions, (versionString) => !versionString.includes('SNAPSHOT'))
);
let version =
semver.maxSatisfying(agentVersions, `<=${kbnStatus.version.number}`) ??
kbnStatus.version.number;
// Add `-SNAPSHOT` if version indicates it was from a snapshot or the build hash starts
// with `xxxxxxxxx` (value that seems to be present when running kibana from source)
if (
kbnStatus.version.build_snapshot ||
kbnStatus.version.build_hash.startsWith('XXXXXXXXXXXXXXX')
) {
version += '-SNAPSHOT';
}
return version;
};