[Defend Workflows] Use Vagrant for real agent Cypress e2e (#160050)

## Summary

Run Real Endpoint Cypress E2E on CI using Vagrant

---------

Co-authored-by: Tomasz Ciecierski <ciecierskitomek@gmail.com>
Co-authored-by: Ashokaditya <am.struktr@gmail.com>
This commit is contained in:
Patryk Kopyciński 2023-07-11 12:02:51 +02:00 committed by GitHub
parent bda0195982
commit ba539d7a39
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 799 additions and 623 deletions

View file

@ -28,7 +28,6 @@ disabled:
- 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/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

@ -14,3 +14,19 @@ steps:
limit: 1
artifact_paths:
- "target/kibana-security-solution/**/*"
- command: .buildkite/scripts/steps/functional/defend_workflows_vagrant.sh
label: 'Defend Workflows Endpoint Cypress Tests'
agents:
queue: n2-16-virt
depends_on: build
timeout_in_minutes: 120
parallelism: 6
retry:
automatic:
- exit_status: '-1'
limit: 3
- exit_status: '*'
limit: 1
artifact_paths:
- "target/kibana-security-solution/**/*"

View file

@ -0,0 +1,4 @@
#!/usr/bin/env bash
Xvfb -screen 0 1680x946x24 :99 &
export DISPLAY=:99

View file

@ -3,14 +3,11 @@
set -euo pipefail
source .buildkite/scripts/steps/functional/common.sh
source .buildkite/scripts/steps/functional/common_cypress.sh
export JOB=kibana-defend-workflows-cypress
export KIBANA_INSTALL_DIR=${KIBANA_BUILD_LOCATION}
Xvfb -screen 0 1680x946x24 :99 &
export DISPLAY=:99
echo "--- Defend Workflows Cypress tests"
yarn --cwd x-pack/plugins/security_solution cypress:dw:run

View file

@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
source .buildkite/scripts/steps/functional/common.sh
source .buildkite/scripts/steps/functional/common_cypress.sh
export JOB=kibana-defend-workflows-endpoint-cypress
export KIBANA_INSTALL_DIR=${KIBANA_BUILD_LOCATION}
echo "--- Defend Workflows Endpoint Cypress tests"
yarn --cwd x-pack/plugins/security_solution cypress:dw:endpoint:run

View file

@ -3,14 +3,11 @@
set -euo pipefail
source .buildkite/scripts/common/util.sh
source .buildkite/scripts/steps/functional/common_cypress.sh
.buildkite/scripts/bootstrap.sh
node scripts/build_kibana_platform_plugins.js
Xvfb :99 -screen 0 1600x1200x24 &
export DISPLAY=:99
export JOB=kibana-osquery-cypress
echo "--- Osquery Cypress tests"

View file

@ -3,14 +3,11 @@
set -euo pipefail
source .buildkite/scripts/steps/functional/common.sh
source .buildkite/scripts/steps/functional/common_cypress.sh
export JOB=kibana-security-solution-chrome
export KIBANA_INSTALL_DIR=${KIBANA_BUILD_LOCATION}
Xvfb -screen 0 1680x946x24 :99 &
export DISPLAY=:99
echo "--- Response Ops Cypress Tests on Security Solution"
yarn --cwd x-pack/plugins/security_solution cypress:run:respops

View file

@ -3,14 +3,11 @@
set -euo pipefail
source .buildkite/scripts/steps/functional/common.sh
source .buildkite/scripts/steps/functional/common_cypress.sh
export JOB=kibana-security-solution-chrome
export KIBANA_INSTALL_DIR=${KIBANA_BUILD_LOCATION}
Xvfb -screen 0 1680x946x24 :99 &
export DISPLAY=:99
echo "--- Response Ops Cases Cypress Tests on Security Solution"
yarn --cwd x-pack/plugins/security_solution cypress:run:cases

View file

@ -3,14 +3,11 @@
set -euo pipefail
source .buildkite/scripts/steps/functional/common.sh
source .buildkite/scripts/steps/functional/common_cypress.sh
export JOB=kibana-security-solution-chrome
export KIBANA_INSTALL_DIR=${KIBANA_BUILD_LOCATION}
Xvfb :99 -screen 0 1600x1200x24 &
export DISPLAY=:99
echo "--- Security Solution tests (Chrome)"
yarn --cwd x-pack/plugins/security_solution cypress:run

View file

@ -3,15 +3,11 @@
set -euo pipefail
source .buildkite/scripts/steps/functional/common.sh
source .buildkite/scripts/steps/functional/common_cypress.sh
export JOB=kibana-security-solution-chrome
export KIBANA_INSTALL_DIR=${KIBANA_BUILD_LOCATION}
Xvfb :99 -screen 0 1600x1200x24 &
export DISPLAY=:99
echo "--- Explore Cypress Tests on Security Solution"
yarn --cwd x-pack/plugins/security_solution cypress:explore:run

View file

@ -3,15 +3,11 @@
set -euo pipefail
source .buildkite/scripts/steps/functional/common.sh
source .buildkite/scripts/steps/functional/common_cypress.sh
export JOB=kibana-security-solution-chrome
export KIBANA_INSTALL_DIR=${KIBANA_BUILD_LOCATION}
Xvfb :99 -screen 0 1600x1200x24 &
export DISPLAY=:99
echo "--- Investigations Cypress Tests on Security Solution"
yarn --cwd x-pack/plugins/security_solution cypress:investigations:run

View file

@ -3,14 +3,11 @@
set -euo pipefail
source .buildkite/scripts/steps/functional/common.sh
source .buildkite/scripts/steps/functional/common_cypress.sh
export JOB=kibana-threat-intelligence-chrome
export KIBANA_INSTALL_DIR=${KIBANA_BUILD_LOCATION}
Xvfb :99 -screen 0 1600x1200x24 &
export DISPLAY=:99
echo "--- Threat Intelligence tests (Chrome)"
yarn --cwd x-pack/plugins/threat_intelligence cypress:run

View file

@ -44,6 +44,7 @@ export const IGNORE_FILE_GLOBS = [
'packages/kbn-test/jest-preset.js',
'packages/kbn-test/*/jest-preset.js',
'test/package/Vagrantfile',
'x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/Vagrantfile',
'**/test/**/fixtures/**/*',
// Required to match the name in the docs.elastic.dev repo.

View file

@ -1,14 +0,0 @@
#!/usr/bin/env bash
source test/scripts/jenkins_test_setup_xpack.sh
echo " -> Running defend workflows cypress tests"
cd "$XPACK_DIR"
node scripts/functional_tests \
--debug --bail \
--kibana-install-dir "$KIBANA_INSTALL_DIR" \
--config test/defend_workflows_cypress/cli_config.ts
echo ""
echo ""

View file

@ -21,8 +21,8 @@
"cypress:run:upgrade": "yarn cypress:run:reporter --browser chrome --config specPattern=./cypress/upgrade_e2e/**/*.cy.ts",
"cypress:dw:open": "node ./scripts/start_cypress_parallel open --config-file ./public/management/cypress.config.ts ts --ftr-config-file ../../../../../../x-pack/test/defend_workflows_cypress/cli_config",
"cypress:dw:run": "node ./scripts/start_cypress_parallel run --config-file ./public/management/cypress.config.ts --ftr-config-file ../../../../../../x-pack/test/defend_workflows_cypress/cli_config --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json; status=$?; yarn junit:merge && exit $status",
"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",
"cypress:dw:endpoint:run": "node ./scripts/start_cypress_parallel run --config-file ./public/management/cypress_endpoint.config.ts --ftr-config-file ../../../../../../x-pack/test/defend_workflows_cypress/endpoint_config --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json --concurrency 1; status=$?; yarn junit:merge && exit $status",
"cypress:dw:endpoint:open": "node ./scripts/start_cypress_parallel open --config-file ./public/management/cypress_endpoint.config.ts ts --ftr-config-file ../../../../../../x-pack/test/defend_workflows_cypress/endpoint_config",
"cypress:investigations:run": "yarn cypress:run:reporter --browser chrome --spec './cypress/e2e/investigations/**/*.cy.ts' --ftr-config-file ../../../../../../x-pack/test/security_solution_cypress/cli_config; status=$?; yarn junit:merge && exit $status",
"cypress:explore:run": "yarn cypress:run:reporter --browser chrome --spec './cypress/e2e/explore/**/*.cy.ts' --ftr-config-file ../../../../../../x-pack/test/security_solution_cypress/cli_config; status=$?; yarn junit:merge && exit $status",
"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/",

View file

@ -1,14 +1,43 @@
# Cypress Tests
The `management/cypress` directory contains functional UI tests that execute using [Cypress](https://www.cypress.io/).
The `plugins/security_solution/public/management/cypress` directory contains functional UI tests that execute
using [Cypress](https://www.cypress.io/).
## Pre-requisites
Good to have before you run the tests:
- Docker CLI, as Docker desktop doesn't install it by default. Install using `brew`:
```shell
brew install docker
```
- [Multipass](https://multipass.run/) for running the tests against real endpoint. Install using `brew`:
```shell
brew install multipass
```
If you also want to run the tests against real endpoints as on the CI pipeline, then you need to have the following:
- [Vagrant](https://developer.hashicorp.com/vagrant/docs/installation)
- [Vagrant provider for VMware](https://developer.hashicorp.com/vagrant/docs/providers/vmware/installation)
- [Vagrant VMware Utility](https://developer.hashicorp.com/vagrant/docs/providers/vmware/vagrant-vmware-utility)
- [VMware Fusion](https://www.vmware.com/products/fusion/fusion-evaluation.html)
See [running interactive tests on real endpoint with vagrant](#cypress-interactive-with-real-endpoints-using-vagrant)
for more information.
## Running the tests
There are currently three ways to run the tests, comprised of two execution modes and two target environments, which will be detailed below.
There are currently three ways to run the tests, comprised of two execution modes and two target environments, which
will be detailed below.
### Environment Variables
The test suites are setup with defaults for Kibana and Elasticsearch. The following environment variables can be set to changes those defaults and target a run against different instances of the stack:
The test suites are set up with defaults for Kibana and Elasticsearch. The following environment variables can be set to
changes those defaults and target a run against different instances of the stack:
```
CYPRESS_KIBANA_URL
@ -27,68 +56,143 @@ Some notes:
Example:
```shell
cd /x-pack/plugins/security_solution
yarn --cwd x-pack/plugins/security_solution
CYPRESS_BASE_URL=http://localhost:5601 \
CYPRESS_KIBANA_URL=http://localhost:5601 \
CYPRESS_ELASTICSEARCH_USERNAME=elastic \
CYPRESS_ELASTICSEARCH_PASSWORD=changeme \
CYPRESS_ELASTICSEARCH_URL=http://localhost:9200 yarn cypress:dw:open
CYPRESS_ELASTICSEARCH_URL=http://localhost:9200 cypress:dw:open
```
### Execution modes
#### Interactive mode
When you run Cypress in interactive mode, an interactive runner is displayed that allows you to see commands as they execute while also viewing the application under test. For more information, please see [cypress documentation](https://docs.cypress.io/guides/core-concepts/test-runner.html#Overview).
When you run Cypress in interactive mode, an interactive runner is displayed that allows you to see commands as they
execute while also viewing the application under test. For more information, please
see [cypress documentation](https://docs.cypress.io/guides/core-concepts/test-runner.html#Overview).
#### Headless mode
A headless browser is a browser simulation program that does not have a user interface. These programs operate like any other browser but do not display any UI. This is why meanwhile you are executing the tests on this mode you are not going to see the application under test. Just the output of the test is displayed on the terminal once the execution is finished.
A headless browser is a browser simulation program that does not have a user interface. These programs operate like any
other browser but do not display any UI. So when you are executing the tests on this mode you are not
going to see the application under test. Just the output of the test is displayed on the terminal once the execution is
finished.
### Target environments
#### FTR (CI)
This is the configuration used by CI. It uses the FTR to spawn both a Kibana instance (http://localhost:5620) and an Elasticsearch instance (http://localhost:9220) with a preloaded minimum set of data (see preceding "Test data" section), and then executes cypress against this stack. You can find this configuration in `x-pack/test/defend_workflows_cypress`
This is the configuration used by CI. It uses the FTR to spawn both a Kibana instance (http://localhost:5620) and an
Elasticsearch instance (http://localhost:9220) with a preloaded minimum set of data (see preceding "Test data" section),
and then executes cypress against this stack. You can find this configuration in `x-pack/test/defend_workflows_cypress`
### Test Execution: Examples
#### FTR + Headless (Chrome)
#### Cypress + Headless (Chrome)
Since this is how tests are run on CI, this will likely be the configuration you want to reproduce failures locally, etc.
Since this is how tests are run on CI, this will likely be the configuration you want to use in order to reproduce
failures locally, etc.
```shell
# bootstrap kibana from the project root
yarn kbn bootstrap
# build the plugins/assets that cypress will execute against
node scripts/build_kibana_platform_plugins
# bootstrap kibana from the project root and build the plugins/assets that cypress will execute against
yarn kbn bootstrap && node scripts/build_kibana_platform_plugins
# launch the cypress test runner
cd x-pack/plugins/security_solution
yarn cypress:dw:run-as-ci
```
#### FTR + Interactive
This is the preferred mode for developing new tests.
# or
# launch without changing directory from kibana/
yarn --cwd x-pack/plugins/security_solution cypress:dw:run-as-ci
```
#### Cypress
This is the preferred mode for developing new tests against mocked data
```shell
# bootstrap kibana from the project root
yarn kbn bootstrap
# build the plugins/assets that cypress will execute against
node scripts/build_kibana_platform_plugins
# bootstrap kibana from the project root and build the plugins/assets that cypress will execute against
yarn kbn bootstrap && node scripts/build_kibana_platform_plugins
# launch the cypress test runner
cd x-pack/plugins/security_solution
yarn cypress:dw:open-as-ci
yarn cypress:dw:open
# or
# launch without changing directory from kibana/
yarn --cwd x-pack/plugins/security_solution cypress:dw:open
```
For developing/debugging tests against real endpoint please use:
Endpoint tests require [Multipass](https://multipass.run/) to be installed on your machine.
```shell
# bootstrap kibana from the project root and build the plugins/assets that cypress will execute against
yarn kbn bootstrap && node scripts/build_kibana_platform_plugins
# launch the cypress test runner with real endpoint
cd x-pack/plugins/security_solution
yarn cypress:dw:endpoint:open
# or
# launch without changing directory from kibana/
yarn --cwd x-pack/plugins/security_solution cypress:dw:endpoint:open
```
#### Cypress (interactive) with real Endpoints using Vagrant
```shell
# bootstrap kibana from the project root and build the plugins/assets that cypress will execute against
yarn kbn bootstrap && node scripts/build_kibana_platform_plugins
# launch the cypress test runner with real endpoint
cd x-pack/plugins/security_solution
export CI=true
yarn cypress:dw:endpoint:open
````
Note that you can select the browser you want to use on the top right side of the interactive runner.
#### Cypress against REAL Endpoint + Headless (Chrome)
This requires some additional setup as mentioned in the [pre-requisites](#pre-requisites) section.
Endpoint tests require [Multipass](https://multipass.run/) to be installed on your machine.
```shell
# bootstrap kibana from the project root and build the plugins/assets that cypress will execute against
yarn kbn bootstrap && node scripts/build_kibana_platform_plugins
# launch the cypress test runner with real endpoint
cd x-pack/plugins/security_solution
yarn cypress:dw:endpoint:run
# or
# launch without changing directory from kibana/
yarn --cwd x-pack/plugins/security_solution cypress:dw:endpoint:run
```
## Folder Structure
### e2e/
Contains all the tests. Within it are two sub-folders:
#### cypress/endpoint
Contains all the tests that are executed against real endpoints.
#### cypress/mocked_data
Contains all the tests that are executed against mocked endpoint and run on CI. If you want to add tests that run on CI
then this is where you should add those.
### integration/
Cypress convention. Contains the specs that are going to be executed.
@ -99,7 +203,8 @@ Cypress convention. Fixtures are used as external pieces of static data when we
### support/
Cypress convention. As a convenience, by default Cypress will automatically include the plugins file cypress/plugins/index.js before every single spec file it runs.
Cypress convention. As a convenience, by default Cypress will automatically include the plugins file
cypress/plugins/index.js before every single spec file it runs.
Directory also holds Cypress Plugins that are then initialized via `setupNodeEvents()` in the Cypress configuration.
### screens/
@ -112,7 +217,7 @@ Each file inside the screens folder represents a screen in our application.
_Tasks_ are functions that may be reused across tests.
Each file inside the tasks folder represents a screen of our application.
Each file inside the tasks folder represents a screen of our application.
## Test data
@ -122,9 +227,10 @@ The data the tests need:
## Development Best Practices
### Clean up the state
### Clean up the state
Remember to clean up the state of the test after its execution. Be mindful of failure scenarios, as well: if your test fails, will it leave the environment in a recoverable state?
Remember to clean up the state of the test after its execution. Be mindful of failure scenarios, as well: if your test
fails, will it leave the environment in a recoverable state?
### Minimize the use of es_archive
@ -143,4 +249,5 @@ Remember that by minimizing the number of times the web page is loaded, we minim
## Linting
Optional linting rules for Cypress and linting setup can be found [here](https://github.com/cypress-io/eslint-plugin-cypress#usage)
Optional linting rules for Cypress and linting setup can be
found [here](https://github.com/cypress-io/eslint-plugin-cypress#usage)

View file

@ -6,32 +6,57 @@
*/
import { recurse } from 'cypress-recurse';
import type { IndexedFleetEndpointPolicyResponse } from '../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy';
import { HOST_METADATA_LIST_ROUTE } from '../../../../../common/endpoint/constants';
import type { MetadataListResponse } from '../../../../../common/endpoint/types';
import type { MetadataListResponse, PolicyData } from '../../../../../common/endpoint/types';
import { APP_ENDPOINTS_PATH } from '../../../../../common/constants';
import { getArtifactsListTestsData } from '../../fixtures/artifacts_page';
import { removeAllArtifacts } from '../../tasks/artifacts';
import { loadEndpointDataForEventFiltersIfNeeded } from '../../tasks/load_endpoint_data';
import { login } from '../../tasks/login';
import { performUserActions } from '../../tasks/perform_user_actions';
import { request } from '../../tasks/common';
import { yieldEndpointPolicyRevision } from '../../tasks/fleet';
import { request, loadPage } from '../../tasks/common';
import {
createAgentPolicyTask,
getEndpointIntegrationVersion,
yieldEndpointPolicyRevision,
} from '../../tasks/fleet';
import type { CreateAndEnrollEndpointHostResponse } from '../../../../../scripts/endpoint/common/endpoint_host_services';
import { createEndpointHost } from '../../tasks/create_endpoint_host';
import { deleteAllLoadedEndpointData } from '../../tasks/delete_all_endpoint_data';
import { enableAllPolicyProtections } from '../../tasks/endpoint_policy';
const yieldAppliedEndpointRevision = (): Cypress.Chainable<number> =>
request<MetadataListResponse>({
method: 'GET',
url: HOST_METADATA_LIST_ROUTE,
}).then(({ body }) => {
expect(body.data.length).is.lte(1); // during update it can be temporary zero
expect(body.data.length).is.lte(2); // during update it can be temporary zero
return Number(body.data?.[0]?.metadata.Endpoint.policy.applied.endpoint_policy_version) ?? -1;
});
const parseRevNumber = (revString: string) => Number(revString.match(/\d+/)?.[0]);
describe('Artifact pages', () => {
let indexedPolicy: IndexedFleetEndpointPolicyResponse;
let policy: PolicyData;
let createdHost: CreateAndEnrollEndpointHostResponse;
before(() => {
getEndpointIntegrationVersion().then((version) =>
createAgentPolicyTask(version).then((data) => {
indexedPolicy = data;
policy = indexedPolicy.integrationPolicies[0];
return enableAllPolicyProtections(policy.id).then(() => {
// Create and enroll a new Endpoint host
return createEndpointHost(policy.policy_id).then((host) => {
createdHost = host as CreateAndEnrollEndpointHostResponse;
});
});
})
);
login();
loadEndpointDataForEventFiltersIfNeeded();
removeAllArtifacts();
// wait for ManifestManager to pick up artifact changes that happened either here
@ -49,11 +74,23 @@ describe('Artifact pages', () => {
beforeEach(() => {
login();
cy.visit(APP_ENDPOINTS_PATH);
loadPage(APP_ENDPOINTS_PATH);
});
after(() => {
removeAllArtifacts();
if (createdHost) {
cy.task('destroyEndpointHost', createdHost);
}
if (indexedPolicy) {
cy.task('deleteIndexedFleetEndpointPolicies', indexedPolicy);
}
if (createdHost) {
deleteAllLoadedEndpointData({ endpointAgentIds: [createdHost.agentId] });
}
});
for (const testData of getArtifactsListTestsData()) {
@ -64,7 +101,7 @@ describe('Artifact pages', () => {
.invoke('text')
.then(parseRevNumber)
.then((initialRevisionNumber) => {
cy.visit(`/app/security/administration/${testData.urlPath}`);
loadPage(`/app/security/administration/${testData.urlPath}`);
cy.getByTestSubj(`${testData.pagePrefix}-emptyState-addButton`).click();
performUserActions(testData.create.formActions);
@ -75,7 +112,7 @@ describe('Artifact pages', () => {
cy.getByTestSubj(checkResult.selector).should('have.text', checkResult.value);
}
cy.visit(APP_ENDPOINTS_PATH);
loadPage(APP_ENDPOINTS_PATH);
// depends on the 10s auto refresh
cy.getByTestSubj('policyListRevNo')

View file

@ -5,25 +5,57 @@
* 2.0.
*/
import type { Agent } from '@kbn/fleet-plugin/common';
import type { PolicyData } from '../../../../../common/endpoint/types';
import { APP_ENDPOINTS_PATH } from '../../../../../common/constants';
import { closeAllToasts } from '../../tasks/toasts';
import { toggleRuleOffAndOn, visitRuleAlerts } from '../../tasks/isolate';
import { cleanupRule, loadRule } from '../../tasks/api_fixtures';
import { ENDPOINT_VM_NAME } from '../../tasks/common';
import { login } from '../../tasks/login';
import { loadPage } from '../../tasks/common';
import type { IndexedFleetEndpointPolicyResponse } from '../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy';
import {
createAgentPolicyTask,
getAgentByHostName,
getEndpointIntegrationVersion,
reassignAgentPolicy,
} from '../../tasks/fleet';
import { createAgentPolicyTask, getEndpointIntegrationVersion } from '../../tasks/fleet';
import { changeAlertsFilter } from '../../tasks/alerts';
import type { CreateAndEnrollEndpointHostResponse } from '../../../../../scripts/endpoint/common/endpoint_host_services';
import { createEndpointHost } from '../../tasks/create_endpoint_host';
import { deleteAllLoadedEndpointData } from '../../tasks/delete_all_endpoint_data';
import { enableAllPolicyProtections } from '../../tasks/endpoint_policy';
describe('Automated Response Actions', () => {
const endpointHostname = Cypress.env(ENDPOINT_VM_NAME);
const hostname = Cypress.env('hostname');
let indexedPolicy: IndexedFleetEndpointPolicyResponse;
let policy: PolicyData;
let createdHost: CreateAndEnrollEndpointHostResponse;
before(() => {
getEndpointIntegrationVersion().then((version) =>
createAgentPolicyTask(version, 'automated_response_actions').then((data) => {
indexedPolicy = data;
policy = indexedPolicy.integrationPolicies[0];
return enableAllPolicyProtections(policy.id).then(() => {
// Create and enroll a new Endpoint host
return createEndpointHost(policy.policy_id).then((host) => {
createdHost = host as CreateAndEnrollEndpointHostResponse;
});
});
})
);
});
after(() => {
if (createdHost) {
cy.task('destroyEndpointHost', createdHost);
}
if (indexedPolicy) {
cy.task('deleteIndexedFleetEndpointPolicies', indexedPolicy);
}
if (createdHost) {
deleteAllLoadedEndpointData({ endpointAgentIds: [createdHost.agentId] });
}
});
const hostname = new URL(Cypress.env('FLEET_SERVER_URL')).port;
const fleetHostname = `dev-fleet-server.${hostname}`;
beforeEach(() => {
@ -31,47 +63,28 @@ describe('Automated Response Actions', () => {
});
describe('From alerts', () => {
let response: IndexedFleetEndpointPolicyResponse;
let initialAgentData: Agent;
let ruleId: string;
let ruleName: string;
before(() => {
getAgentByHostName(endpointHostname).then((agentData) => {
initialAgentData = agentData;
});
getEndpointIntegrationVersion().then((version) => {
createAgentPolicyTask(version).then((data) => {
response = data;
});
});
loadRule(true).then((data) => {
loadRule().then((data) => {
ruleId = data.id;
ruleName = data.name;
});
});
after(() => {
if (initialAgentData?.policy_id) {
reassignAgentPolicy(initialAgentData.id, initialAgentData.policy_id);
}
if (response) {
cy.task('deleteIndexedFleetEndpointPolicies', response);
}
if (ruleId) {
cleanupRule(ruleId);
}
});
it('should have generated endpoint and rule', () => {
cy.visit(APP_ENDPOINTS_PATH);
cy.contains(endpointHostname).should('exist');
loadPage(APP_ENDPOINTS_PATH);
cy.contains(createdHost.hostname).should('exist');
toggleRuleOffAndOn(ruleName);
});
it('should display endpoint automated response action in event details flyout ', () => {
visitRuleAlerts(ruleName);
closeAllToasts();
@ -80,9 +93,9 @@ describe('Automated Response Actions', () => {
cy.getByTestSubj('responseActionsViewTab').click();
cy.getByTestSubj('response-actions-notification').should('not.have.text', '0');
cy.getByTestSubj(`response-results-${endpointHostname}-details-tray`)
cy.getByTestSubj(`response-results-${createdHost.hostname}-details-tray`)
.should('contain', 'isolate completed successfully')
.and('contain', endpointHostname);
.and('contain', createdHost.hostname);
cy.getByTestSubj(`response-results-${fleetHostname}-details-tray`)
.should('contain', 'The host does not have Elastic Defend integration installed')

View file

@ -42,7 +42,7 @@ describe('Endpoint generated alerts', () => {
after(() => {
if (createdHost) {
cy.task('destroyEndpointHost', createdHost).then(() => {});
cy.task('destroyEndpointHost', createdHost);
}
if (indexedPolicy) {

View file

@ -6,8 +6,8 @@
*/
import type { Agent } from '@kbn/fleet-plugin/common';
import type { PolicyData } from '../../../../../common/endpoint/types';
import { APP_ENDPOINTS_PATH } from '../../../../../common/constants';
import { ENDPOINT_VM_NAME } from '../../tasks/common';
import {
createAgentPolicyTask,
getAgentByHostName,
@ -16,6 +16,7 @@ import {
} from '../../tasks/fleet';
import type { IndexedFleetEndpointPolicyResponse } from '../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy';
import { login } from '../../tasks/login';
import { loadPage } from '../../tasks/common';
import {
AGENT_HOSTNAME_CELL,
TABLE_ROW_ACTIONS,
@ -26,18 +27,56 @@ import {
FLEET_REASSIGN_POLICY_MODAL,
FLEET_REASSIGN_POLICY_MODAL_CONFIRM_BUTTON,
} from '../../screens/fleet';
import type { CreateAndEnrollEndpointHostResponse } from '../../../../../scripts/endpoint/common/endpoint_host_services';
import { createEndpointHost } from '../../tasks/create_endpoint_host';
import { deleteAllLoadedEndpointData } from '../../tasks/delete_all_endpoint_data';
import { enableAllPolicyProtections } from '../../tasks/endpoint_policy';
describe('Endpoints page', () => {
const endpointHostname = Cypress.env(ENDPOINT_VM_NAME);
let indexedPolicy: IndexedFleetEndpointPolicyResponse;
let policy: PolicyData;
let createdHost: CreateAndEnrollEndpointHostResponse;
before(() => {
getEndpointIntegrationVersion().then((version) => {
createAgentPolicyTask(version).then((data) => {
indexedPolicy = data;
policy = indexedPolicy.integrationPolicies[0];
return enableAllPolicyProtections(policy.id).then(() => {
// Create and enroll a new Endpoint host
return createEndpointHost(policy.policy_id).then((host) => {
createdHost = host as CreateAndEnrollEndpointHostResponse;
});
});
});
});
});
after(() => {
if (createdHost) {
cy.task('destroyEndpointHost', createdHost);
}
if (indexedPolicy) {
cy.task('deleteIndexedFleetEndpointPolicies', indexedPolicy);
}
if (createdHost) {
deleteAllLoadedEndpointData({ endpointAgentIds: [createdHost.agentId] });
}
});
beforeEach(() => {
login();
});
it('Shows endpoint on the list', () => {
cy.visit(APP_ENDPOINTS_PATH);
loadPage(APP_ENDPOINTS_PATH);
cy.contains('Hosts running Elastic Defend').should('exist');
cy.getByTestSubj(AGENT_HOSTNAME_CELL).should('have.text', endpointHostname);
cy.getByTestSubj(AGENT_HOSTNAME_CELL)
.contains(createdHost.hostname)
.should('have.text', createdHost.hostname);
});
describe('Endpoint reassignment', () => {
@ -45,7 +84,7 @@ describe('Endpoints page', () => {
let initialAgentData: Agent;
before(() => {
getAgentByHostName(endpointHostname).then((agentData) => {
getAgentByHostName(createdHost.hostname).then((agentData) => {
initialAgentData = agentData;
});
getEndpointIntegrationVersion().then((version) => {
@ -69,24 +108,24 @@ describe('Endpoints page', () => {
});
it('User can reassign a single endpoint to a different Agent Configuration', () => {
cy.visit(APP_ENDPOINTS_PATH);
loadPage(APP_ENDPOINTS_PATH);
const hostname = cy
.getByTestSubj(AGENT_HOSTNAME_CELL)
.filter(`:contains("${endpointHostname}")`);
.filter(`:contains("${createdHost.hostname}")`);
const tableRow = hostname.parents('tr');
tableRow.getByTestSubj(TABLE_ROW_ACTIONS).click();
tableRow.findByTestSubj(TABLE_ROW_ACTIONS).click();
cy.getByTestSubj(TABLE_ROW_ACTIONS_MENU).contains('Reassign agent policy').click();
cy.getByTestSubj(FLEET_REASSIGN_POLICY_MODAL)
.find('select')
.select(response.agentPolicies[0].name);
cy.getByTestSubj(FLEET_REASSIGN_POLICY_MODAL_CONFIRM_BUTTON).click();
cy.getByTestSubj(AGENT_HOSTNAME_CELL)
.filter(`:contains("${endpointHostname}")`)
.filter(`:contains("${createdHost.hostname}")`)
.should('exist');
cy.getByTestSubj(AGENT_HOSTNAME_CELL)
.filter(`:contains("${endpointHostname}")`)
.filter(`:contains("${createdHost.hostname}")`)
.parents('tr')
.getByTestSubj(AGENT_POLICY_CELL)
.findByTestSubj(AGENT_POLICY_CELL)
.should('have.text', response.agentPolicies[0].name);
});
});
@ -94,7 +133,7 @@ describe('Endpoints page', () => {
it('should update endpoint policy on Endpoint', () => {
const parseRevNumber = (revString: string) => Number(revString.match(/\d+/)?.[0]);
cy.visit(APP_ENDPOINTS_PATH);
loadPage(APP_ENDPOINTS_PATH);
cy.getByTestSubj('policyListRevNo')
.first()

View file

@ -5,14 +5,13 @@
* 2.0.
*/
import type { Agent } from '@kbn/fleet-plugin/common';
import type { PolicyData } from '../../../../../common/endpoint/types';
import { APP_CASES_PATH, APP_ENDPOINTS_PATH } from '../../../../../common/constants';
import { closeAllToasts } from '../../tasks/toasts';
import {
checkEndpointListForOnlyIsolatedHosts,
checkEndpointListForOnlyUnIsolatedHosts,
checkFlyoutEndpointIsolation,
filterOutEndpoints,
filterOutIsolatedHosts,
isolateHostWithComment,
openAlertDetails,
@ -23,53 +22,63 @@ import {
waitForReleaseOption,
} from '../../tasks/isolate';
import { cleanupCase, cleanupRule, loadCase, loadRule } from '../../tasks/api_fixtures';
import { ENDPOINT_VM_NAME } from '../../tasks/common';
import { login } from '../../tasks/login';
import { loadPage } from '../../tasks/common';
import type { IndexedFleetEndpointPolicyResponse } from '../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy';
import {
createAgentPolicyTask,
getAgentByHostName,
getEndpointIntegrationVersion,
reassignAgentPolicy,
} from '../../tasks/fleet';
import { createAgentPolicyTask, getEndpointIntegrationVersion } from '../../tasks/fleet';
import type { CreateAndEnrollEndpointHostResponse } from '../../../../../scripts/endpoint/common/endpoint_host_services';
import { createEndpointHost } from '../../tasks/create_endpoint_host';
import { deleteAllLoadedEndpointData } from '../../tasks/delete_all_endpoint_data';
import { enableAllPolicyProtections } from '../../tasks/endpoint_policy';
describe('Isolate command', () => {
const endpointHostname = Cypress.env(ENDPOINT_VM_NAME);
const isolateComment = `Isolating ${endpointHostname}`;
const releaseComment = `Releasing ${endpointHostname}`;
let isolateComment: string;
let releaseComment: string;
let indexedPolicy: IndexedFleetEndpointPolicyResponse;
let policy: PolicyData;
let createdHost: CreateAndEnrollEndpointHostResponse;
before(() => {
getEndpointIntegrationVersion().then((version) => {
createAgentPolicyTask(version).then((data) => {
indexedPolicy = data;
policy = indexedPolicy.integrationPolicies[0];
return enableAllPolicyProtections(policy.id).then(() => {
// Create and enroll a new Endpoint host
return createEndpointHost(policy.policy_id).then((host) => {
createdHost = host as CreateAndEnrollEndpointHostResponse;
isolateComment = `Isolating ${host.hostname}`;
releaseComment = `Releasing ${host.hostname}`;
});
});
});
});
});
after(() => {
if (createdHost) {
cy.task('destroyEndpointHost', createdHost);
}
if (indexedPolicy) {
cy.task('deleteIndexedFleetEndpointPolicies', indexedPolicy);
}
if (createdHost) {
deleteAllLoadedEndpointData({ endpointAgentIds: [createdHost.agentId] });
}
});
beforeEach(() => {
login();
});
describe('From manage', () => {
let response: IndexedFleetEndpointPolicyResponse;
let initialAgentData: Agent;
before(() => {
getAgentByHostName(endpointHostname).then((agentData) => {
initialAgentData = agentData;
});
getEndpointIntegrationVersion().then((version) =>
createAgentPolicyTask(version).then((data) => {
response = data;
})
);
});
after(() => {
if (initialAgentData?.policy_id) {
reassignAgentPolicy(initialAgentData.id, initialAgentData.policy_id);
}
if (response) {
cy.task('deleteIndexedFleetEndpointPolicies', response);
}
});
it('should allow filtering endpoint by Isolated status', () => {
cy.visit(APP_ENDPOINTS_PATH);
loadPage(APP_ENDPOINTS_PATH);
closeAllToasts();
cy.getByTestSubj('globalLoadingIndicator-hidden').should('exist');
checkEndpointListForOnlyUnIsolatedHosts();
filterOutIsolatedHosts();
@ -79,11 +88,11 @@ describe('Isolate command', () => {
cy.getByTestSubj('endpointTableRowActions').click();
cy.getByTestSubj('isolateLink').click();
cy.contains(`Isolate host ${endpointHostname} from network.`);
cy.contains(`Isolate host ${createdHost.hostname} from network.`);
cy.getByTestSubj('endpointHostIsolationForm');
cy.getByTestSubj('host_isolation_comment').type(isolateComment);
cy.getByTestSubj('hostIsolateConfirmButton').click();
cy.contains(`Isolation on host ${endpointHostname} successfully submitted`);
cy.contains(`Isolation on host ${createdHost.hostname} successfully submitted`);
cy.getByTestSubj('euiFlyoutCloseButton').click();
cy.getByTestSubj('rowHostStatus-actionStatuses').should('contain.text', 'Isolated');
filterOutIsolatedHosts();
@ -92,7 +101,7 @@ describe('Isolate command', () => {
cy.getByTestSubj('endpointTableRowActions').click();
cy.getByTestSubj('unIsolateLink').click();
releaseHostWithComment(releaseComment, endpointHostname);
releaseHostWithComment(releaseComment, createdHost.hostname);
cy.contains('Confirm').click();
cy.getByTestSubj('euiFlyoutCloseButton').click();
cy.getByTestSubj('adminSearchBar').click().type('{selectall}{backspace}');
@ -102,68 +111,49 @@ describe('Isolate command', () => {
});
describe('From alerts', () => {
let response: IndexedFleetEndpointPolicyResponse;
let initialAgentData: Agent;
let ruleId: string;
let ruleName: string;
before(() => {
getAgentByHostName(endpointHostname).then((agentData) => {
initialAgentData = agentData;
});
getEndpointIntegrationVersion().then((version) =>
createAgentPolicyTask(version).then((data) => {
response = data;
})
);
loadRule(false).then((data) => {
loadRule(
{ query: `agent.name: ${createdHost.hostname} and agent.type: endpoint` },
false
).then((data) => {
ruleId = data.id;
ruleName = data.name;
});
});
after(() => {
if (initialAgentData?.policy_id) {
reassignAgentPolicy(initialAgentData.id, initialAgentData.policy_id);
}
if (response) {
cy.task('deleteIndexedFleetEndpointPolicies', response);
}
if (ruleId) {
cleanupRule(ruleId);
}
});
it('should have generated endpoint and rule', () => {
cy.visit(APP_ENDPOINTS_PATH);
cy.contains(endpointHostname).should('exist');
it('should isolate and release host', () => {
loadPage(APP_ENDPOINTS_PATH);
cy.contains(createdHost.hostname).should('exist');
toggleRuleOffAndOn(ruleName);
});
it('should isolate and release host', () => {
visitRuleAlerts(ruleName);
filterOutEndpoints(endpointHostname);
closeAllToasts();
openAlertDetails();
isolateHostWithComment(isolateComment, endpointHostname);
isolateHostWithComment(isolateComment, createdHost.hostname);
cy.getByTestSubj('hostIsolateConfirmButton').click();
cy.contains(`Isolation on host ${endpointHostname} successfully submitted`);
cy.contains(`Isolation on host ${createdHost.hostname} successfully submitted`);
cy.getByTestSubj('euiFlyoutCloseButton').click();
openAlertDetails();
checkFlyoutEndpointIsolation();
releaseHostWithComment(releaseComment, endpointHostname);
releaseHostWithComment(releaseComment, createdHost.hostname);
cy.contains('Confirm').click();
cy.contains(`Release on host ${endpointHostname} successfully submitted`);
cy.contains(`Release on host ${createdHost.hostname} successfully submitted`);
cy.getByTestSubj('euiFlyoutCloseButton').click();
openAlertDetails();
cy.getByTestSubj('event-field-agent.status').within(() => {
@ -173,8 +163,6 @@ describe('Isolate command', () => {
});
describe('From cases', () => {
let response: IndexedFleetEndpointPolicyResponse;
let initialAgentData: Agent;
let ruleId: string;
let ruleName: string;
let caseId: string;
@ -182,16 +170,10 @@ describe('Isolate command', () => {
const caseOwner = 'securitySolution';
before(() => {
getAgentByHostName(endpointHostname).then((agentData) => {
initialAgentData = agentData;
});
getEndpointIntegrationVersion().then((version) =>
createAgentPolicyTask(version).then((data) => {
response = data;
})
);
loadRule(false).then((data) => {
loadRule(
{ query: `agent.name: ${createdHost.hostname} and agent.type: endpoint` },
false
).then((data) => {
ruleId = data.id;
ruleName = data.name;
});
@ -205,12 +187,6 @@ describe('Isolate command', () => {
});
after(() => {
if (initialAgentData?.policy_id) {
reassignAgentPolicy(initialAgentData.id, initialAgentData.policy_id);
}
if (response) {
cy.task('deleteIndexedFleetEndpointPolicies', response);
}
if (ruleId) {
cleanupRule(ruleId);
}
@ -219,16 +195,13 @@ describe('Isolate command', () => {
}
});
it('should have generated endpoint and rule', () => {
cy.visit(APP_ENDPOINTS_PATH);
cy.contains(endpointHostname).should('exist');
it('should isolate and release host', () => {
loadPage(APP_ENDPOINTS_PATH);
cy.contains(createdHost.hostname).should('exist');
toggleRuleOffAndOn(ruleName);
});
it('should isolate and release host', () => {
visitRuleAlerts(ruleName);
filterOutEndpoints(endpointHostname);
closeAllToasts();
openAlertDetails();
@ -238,13 +211,13 @@ describe('Isolate command', () => {
cy.contains(`An alert was added to \"Test ${caseOwner} case`);
cy.intercept('GET', `/api/cases/${caseId}/user_actions/_find*`).as('case');
cy.visit(`${APP_CASES_PATH}/${caseId}`);
loadPage(`${APP_CASES_PATH}/${caseId}`);
cy.wait('@case', { timeout: 30000 }).then(({ response: res }) => {
const caseAlertId = res?.body.userActions[1].id;
closeAllToasts();
openCaseAlertDetails(caseAlertId);
isolateHostWithComment(isolateComment, endpointHostname);
isolateHostWithComment(isolateComment, createdHost.hostname);
cy.getByTestSubj('hostIsolateConfirmButton').click();
cy.getByTestSubj('euiFlyoutCloseButton').click();
@ -257,11 +230,11 @@ describe('Isolate command', () => {
waitForReleaseOption(caseAlertId);
releaseHostWithComment(releaseComment, endpointHostname);
releaseHostWithComment(releaseComment, createdHost.hostname);
cy.contains('Confirm').click();
cy.contains(`Release on host ${endpointHostname} successfully submitted`);
cy.contains(`Release on host ${createdHost.hostname} successfully submitted`);
cy.getByTestSubj('euiFlyoutCloseButton').click();
cy.getByTestSubj('user-actions-list').within(() => {

View file

@ -23,14 +23,11 @@ import {
} from '../../tasks/isolate';
import { login } from '../../tasks/login';
import { ENDPOINT_VM_NAME } from '../../tasks/common';
import { enableAllPolicyProtections } from '../../tasks/endpoint_policy';
import { createEndpointHost } from '../../tasks/create_endpoint_host';
import { deleteAllLoadedEndpointData } from '../../tasks/delete_all_endpoint_data';
describe('Response console', () => {
const endpointHostname = Cypress.env(ENDPOINT_VM_NAME);
beforeEach(() => {
login();
});
@ -58,7 +55,7 @@ describe('Response console', () => {
after(() => {
if (createdHost) {
cy.task('destroyEndpointHost', createdHost).then(() => {});
cy.task('destroyEndpointHost', createdHost);
}
if (indexedPolicy) {
@ -72,25 +69,25 @@ describe('Response console', () => {
it('should isolate host from response console', () => {
const command = 'isolate';
waitForEndpointListPageToBeLoaded(endpointHostname);
waitForEndpointListPageToBeLoaded(createdHost.hostname);
checkEndpointListForOnlyUnIsolatedHosts();
openResponseConsoleFromEndpointList();
performCommandInputChecks(command);
submitCommand();
waitForCommandToBeExecuted(command);
waitForEndpointListPageToBeLoaded(endpointHostname);
waitForEndpointListPageToBeLoaded(createdHost.hostname);
checkEndpointListForOnlyIsolatedHosts();
});
it('should release host from response console', () => {
const command = 'release';
waitForEndpointListPageToBeLoaded(endpointHostname);
waitForEndpointListPageToBeLoaded(createdHost.hostname);
checkEndpointListForOnlyIsolatedHosts();
openResponseConsoleFromEndpointList();
performCommandInputChecks(command);
submitCommand();
waitForCommandToBeExecuted(command);
waitForEndpointListPageToBeLoaded(endpointHostname);
waitForEndpointListPageToBeLoaded(createdHost.hostname);
checkEndpointListForOnlyUnIsolatedHosts();
});
});
@ -121,7 +118,7 @@ describe('Response console', () => {
after(() => {
if (createdHost) {
cy.task('destroyEndpointHost', createdHost).then(() => {});
cy.task('destroyEndpointHost', createdHost);
}
if (indexedPolicy) {
@ -134,7 +131,7 @@ describe('Response console', () => {
});
it('"processes" - should obtain a list of processes', () => {
waitForEndpointListPageToBeLoaded(endpointHostname);
waitForEndpointListPageToBeLoaded(createdHost.hostname);
openResponseConsoleFromEndpointList();
performCommandInputChecks('processes');
submitCommand();
@ -159,7 +156,7 @@ describe('Response console', () => {
});
it('"kill-process --pid" - should kill a process', () => {
waitForEndpointListPageToBeLoaded(endpointHostname);
waitForEndpointListPageToBeLoaded(createdHost.hostname);
openResponseConsoleFromEndpointList();
inputConsoleCommand(`kill-process --pid ${cronPID}`);
submitCommand();
@ -183,7 +180,7 @@ describe('Response console', () => {
});
it('"suspend-process --pid" - should suspend a process', () => {
waitForEndpointListPageToBeLoaded(endpointHostname);
waitForEndpointListPageToBeLoaded(createdHost.hostname);
openResponseConsoleFromEndpointList();
inputConsoleCommand(`suspend-process --pid ${newCronPID}`);
submitCommand();
@ -191,11 +188,11 @@ describe('Response console', () => {
});
});
describe('File operations: get-file and execute', () => {
const homeFilePath = `/home/ubuntu`;
describe('File operations: get-file and execute', () => {
const homeFilePath = process.env.CI || true ? '/home/vagrant' : `/home/ubuntu`;
const fileContent = 'This is a test file for the get-file command.';
const filePath = `/home/ubuntu/test_file.txt`;
const filePath = `${homeFilePath}/test_file.txt`;
let indexedPolicy: IndexedFleetEndpointPolicyResponse;
let policy: PolicyData;
@ -219,7 +216,7 @@ describe('Response console', () => {
after(() => {
if (createdHost) {
cy.task('destroyEndpointHost', createdHost).then(() => {});
cy.task('destroyEndpointHost', createdHost);
}
if (indexedPolicy) {
@ -232,7 +229,12 @@ describe('Response console', () => {
});
it('"get-file --path" - should retrieve a file', () => {
waitForEndpointListPageToBeLoaded(endpointHostname);
waitForEndpointListPageToBeLoaded(createdHost.hostname);
cy.task('createFileOnEndpoint', {
hostname: createdHost.hostname,
path: filePath,
content: fileContent,
});
openResponseConsoleFromEndpointList();
inputConsoleCommand(`get-file --path ${filePath}`);
submitCommand();
@ -247,14 +249,14 @@ describe('Response console', () => {
cy.readFile(`${downloadsFolder}/upload.zip`);
cy.task('uploadFileToEndpoint', {
hostname: endpointHostname,
hostname: createdHost.hostname,
srcPath: `${downloadsFolder}/upload.zip`,
destPath: '/home/ubuntu/upload.zip',
destPath: `${homeFilePath}/upload.zip`,
});
cy.task('readZippedFileContentOnEndpoint', {
hostname: endpointHostname,
path: '/home/ubuntu/upload.zip',
hostname: createdHost.hostname,
path: `${homeFilePath}/upload.zip`,
password: 'elastic',
}).then((unzippedFileContent) => {
expect(unzippedFileContent).to.equal(fileContent);
@ -262,8 +264,8 @@ describe('Response console', () => {
});
});
it('"execute --command" - should execute a command', async () => {
waitForEndpointListPageToBeLoaded(endpointHostname);
it('"execute --command" - should execute a command', () => {
waitForEndpointListPageToBeLoaded(createdHost.hostname);
openResponseConsoleFromEndpointList();
inputConsoleCommand(`execute --command "ls -al ${homeFilePath}"`);
submitCommand();
@ -294,7 +296,7 @@ describe('Response console', () => {
after(() => {
if (createdHost) {
cy.task('destroyEndpointHost', createdHost).then(() => {});
cy.task('destroyEndpointHost', createdHost);
}
if (indexedPolicy) {
@ -307,19 +309,19 @@ describe('Response console', () => {
});
it('should fail if data tampered', () => {
waitForEndpointListPageToBeLoaded(endpointHostname);
waitForEndpointListPageToBeLoaded(createdHost.hostname);
checkEndpointListForOnlyUnIsolatedHosts();
openResponseConsoleFromEndpointList();
performCommandInputChecks('isolate');
// stop host so that we ensure tamper happens before endpoint processes the action
cy.task('stopEndpointHost');
cy.task('stopEndpointHost', createdHost.hostname);
// get action doc before we submit command so we know when the new action doc is indexed
cy.task('getLatestActionDoc').then((previousActionDoc) => {
submitCommand();
cy.task('tamperActionDoc', previousActionDoc);
});
cy.task('startEndpointHost');
cy.task('startEndpointHost', createdHost.hostname);
const actionValidationErrorMsg =
'Fleet action response error: Failed to validate action signature; check Endpoint logs for details';

View file

@ -12,6 +12,7 @@ import {
loginWithRole,
ROLE,
} from '../../tasks/login';
import { loadPage } from '../../tasks/common';
import { getArtifactsListTestsData } from '../../fixtures/artifacts_page';
import { removeAllArtifacts } from '../../tasks/artifacts';
@ -20,18 +21,18 @@ import { loadEndpointDataForEventFiltersIfNeeded } from '../../tasks/load_endpoi
const loginWithWriteAccess = (url: string) => {
loginWithRole(ROLE.endpoint_security_policy_manager);
cy.visit(url);
loadPage(url);
};
const loginWithReadAccess = (privilegePrefix: string, url: string) => {
const roleWithArtifactReadPrivilege = getRoleWithArtifactReadPrivilege(privilegePrefix);
loginWithCustomRole('roleWithArtifactReadPrivilege', roleWithArtifactReadPrivilege);
cy.visit(url);
loadPage(url);
};
const loginWithoutAccess = (url: string) => {
loginWithRole(ROLE.t1_analyst);
cy.visit(url);
loadPage(url);
};
describe('Artifacts pages', () => {

View file

@ -7,6 +7,7 @@
import { closeAllToasts } from '../../tasks/toasts';
import { login } from '../../tasks/login';
import { loadPage } from '../../tasks/common';
describe('When defining a kibana role for Endpoint security access', () => {
const getAllSubFeatureRows = (): Cypress.Chainable<JQuery<HTMLElement>> => {
@ -18,7 +19,7 @@ describe('When defining a kibana role for Endpoint security access', () => {
beforeEach(() => {
login();
cy.visit('/app/management/security/roles/edit');
loadPage('/app/management/security/roles/edit');
closeAllToasts();
cy.getByTestSubj('addSpacePrivilegeButton').click();
cy.getByTestSubj('featureCategoryButton_securitySolution').closest('button').click();

View file

@ -5,9 +5,11 @@
* 2.0.
*/
import { APP_ENDPOINTS_PATH } from '../../../../../common/constants';
import type { ReturnTypeFromChainable } from '../../types';
import { indexEndpointHosts } from '../../tasks/index_endpoint_hosts';
import { login } from '../../tasks/login';
import { loadPage } from '../../tasks/common';
describe('Endpoints page', () => {
let endpointData: ReturnTypeFromChainable<typeof indexEndpointHosts>;
@ -31,7 +33,7 @@ describe('Endpoints page', () => {
});
it('Loads the endpoints page', () => {
cy.visit('/app/security/administration/endpoints');
loadPage(APP_ENDPOINTS_PATH);
cy.contains('Hosts running Elastic Defend').should('exist');
});
});

View file

@ -24,6 +24,7 @@ import type { ReturnTypeFromChainable } from '../../types';
import { addAlertsToCase } from '../../tasks/add_alerts_to_case';
import { APP_ALERTS_PATH, APP_CASES_PATH, APP_PATH } from '../../../../../common/constants';
import { login } from '../../tasks/login';
import { loadPage } from '../../tasks/common';
import { indexNewCase } from '../../tasks/index_new_case';
import { indexEndpointHosts } from '../../tasks/index_endpoint_hosts';
import { indexEndpointRuleAlerts } from '../../tasks/index_endpoint_rule_alerts';
@ -78,7 +79,7 @@ describe('Isolate command', () => {
});
it('should allow filtering endpoint by Isolated status', () => {
cy.visit(APP_PATH + getEndpointListPath({ name: 'endpointList' }));
loadPage(APP_PATH + getEndpointListPath({ name: 'endpointList' }));
closeAllToasts();
filterOutIsolatedHosts();
isolatedEndpointHostnames.forEach(checkEndpointIsIsolated);
@ -130,7 +131,7 @@ describe('Isolate command', () => {
let isolateRequestResponse: ActionDetails;
let releaseRequestResponse: ActionDetails;
cy.visit(APP_ALERTS_PATH);
loadPage(APP_ALERTS_PATH);
closeAllToasts();
cy.getByTestSubj('alertsTable').within(() => {
@ -256,7 +257,7 @@ describe('Isolate command', () => {
const releaseComment = `Releasing ${hostname}`;
const caseAlertId = caseAlertActions.comments[alertId];
cy.visit(caseUrlPath);
loadPage(caseUrlPath);
closeAllToasts();
openCaseAlertDetails(caseAlertId);

View file

@ -8,6 +8,7 @@
import type { ReturnTypeFromChainable } from '../../types';
import { indexEndpointHosts } from '../../tasks/index_endpoint_hosts';
import { login } from '../../tasks/login';
import { loadPage } from '../../tasks/common';
describe('Response actions history page', () => {
let endpointData: ReturnTypeFromChainable<typeof indexEndpointHosts>;
@ -33,7 +34,7 @@ describe('Response actions history page', () => {
// Flakey, example build failure: https://buildkite.com/elastic/kibana-pull-request/builds/132245
it.skip('retains expanded action details on page reload', () => {
cy.visit(`/app/security/administration/response_actions_history`);
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');

View file

@ -16,6 +16,7 @@ import {
setResponderActionLogDateRange,
} from '../../screens/responder';
import { login } from '../../tasks/login';
import { loadPage } from '../../tasks/common';
import { indexNewCase } from '../../tasks/index_new_case';
import { indexEndpointHosts } from '../../tasks/index_endpoint_hosts';
import { indexEndpointRuleAlerts } from '../../tasks/index_endpoint_rule_alerts';
@ -102,14 +103,14 @@ describe('When accessing Endpoint Response Console', () => {
});
it('should display responder option in take action menu', () => {
cy.visit(caseUrlPath);
loadPage(caseUrlPath);
closeAllToasts();
openCaseAlertDetails();
cy.getByTestSubj('endpointResponseActions-action-item').should('be.enabled');
});
it('should display Responder response action interface', () => {
cy.visit(caseUrlPath);
loadPage(caseUrlPath);
closeAllToasts();
openCaseAlertDetails();
cy.getByTestSubj('endpointResponseActions-action-item').click();

View file

@ -6,9 +6,10 @@
*/
import { APP_ALERTS_PATH } from '../../../../common/constants';
import { loadPage } from '../tasks/common';
export const navigateToAlertsList = (urlQueryParams: string = '') => {
cy.visit(`${APP_ALERTS_PATH}${urlQueryParams ? `?${urlQueryParams}` : ''}`);
loadPage(`${APP_ALERTS_PATH}${urlQueryParams ? `?${urlQueryParams}` : ''}`);
};
export const clickAlertListRefreshButton = (): Cypress.Chainable => {

View file

@ -6,22 +6,17 @@
*/
import { APP_PATH } from '../../../../common/constants';
import { getEndpointDetailsPath, getEndpointListPath } from '../../common/routing';
import { getEndpointDetailsPath } from '../../common/routing';
import { loadPage } from '../tasks/common';
export const AGENT_HOSTNAME_CELL = 'hostnameCellLink';
export const AGENT_POLICY_CELL = 'policyNameCellLink';
export const TABLE_ROW_ACTIONS = 'endpointTableRowActions';
export const TABLE_ROW_ACTIONS_MENU = 'tableRowActionsMenuPanel';
export const navigateToEndpointPolicyResponse = (
endpointAgentId: string
): Cypress.Chainable<Cypress.AUTWindow> => {
return cy.visit(
export const navigateToEndpointPolicyResponse = (endpointAgentId: string): void => {
loadPage(
APP_PATH +
getEndpointDetailsPath({ name: 'endpointPolicyResponse', selected_endpoint: endpointAgentId })
);
};
export const navigateToEndpointList = (): Cypress.Chainable<Cypress.AUTWindow> => {
return cy.visit(APP_PATH + getEndpointListPath({ name: 'endpointList' }));
};

View file

@ -6,18 +6,15 @@
*/
import { FLEET_BASE_PATH } from '@kbn/fleet-plugin/public/constants';
import { loadPage } from '../tasks/common';
export const FLEET_REASSIGN_POLICY_MODAL = 'agentReassignPolicyModal';
export const FLEET_REASSIGN_POLICY_MODAL_CONFIRM_BUTTON = 'confirmModalConfirmButton';
export const navigateToFleetAgentDetails = (
agentId: string
): Cypress.Chainable<Cypress.AUTWindow> => {
export const navigateToFleetAgentDetails = (agentId: string): void => {
// FYI: attempted to use fleet's `pagePathGetters()`, but got compile
// errors due to it pulling too many modules
const response = cy.visit(`${FLEET_BASE_PATH}/agents/${agentId}`);
loadPage(`${FLEET_BASE_PATH}/agents/${agentId}`);
cy.getByTestSubj('agentPolicyNameLink').should('be.visible');
return response;
};

View file

@ -15,11 +15,11 @@ import type {
import { packagePolicyRouteService } from '@kbn/fleet-plugin/common';
import { APP_POLICIES_PATH } from '../../../../common/constants';
import type { PolicyConfig } from '../../../../common/endpoint/types';
import { request } from '../tasks/common';
import { request, loadPage } from '../tasks/common';
import { expectAndCloseSuccessToast } from '../tasks/toasts';
export const visitPolicyDetailsPage = () => {
cy.visit(APP_POLICIES_PATH);
loadPage(APP_POLICIES_PATH);
cy.getByTestSubj('policyNameCellLink').eq(0).click({ force: true });
cy.getByTestSubj('policyDetailsPage').should('exist');

View file

@ -9,6 +9,8 @@
import type { CasePostRequest } from '@kbn/cases-plugin/common/api';
import execa from 'execa';
import { startRuntimeServices } from '../../../../scripts/endpoint/endpoint_agent_runner/runtime';
import { runFleetServerIfNeeded } from '../../../../scripts/endpoint/endpoint_agent_runner/fleet_server';
import {
sendEndpointActionResponse,
sendFleetActionResponse,
@ -50,8 +52,8 @@ import {
startEndpointHost,
createAndEnrollEndpointHost,
destroyEndpointHost,
getEndpointHosts,
stopEndpointHost,
VAGRANT_CWD,
} from '../../../../scripts/endpoint/common/endpoint_host_services';
/**
@ -204,6 +206,8 @@ export const dataLoadersForRealEndpoints = (
on: Cypress.PluginEvents,
config: Cypress.PluginConfigOptions
): void => {
let fleetServerContainerId: string | undefined;
const stackServicesPromise = createRuntimeServices({
kibanaUrl: config.env.KIBANA_URL,
elasticsearchUrl: config.env.ELASTICSEARCH_URL,
@ -213,6 +217,25 @@ export const dataLoadersForRealEndpoints = (
asSuperuser: true,
});
on('before:run', async () => {
await startRuntimeServices({
kibanaUrl: config.env.KIBANA_URL,
elasticUrl: config.env.ELASTICSEARCH_URL,
fleetServerUrl: config.env.FLEET_SERVER_URL,
username: config.env.ELASTICSEARCH_USERNAME,
password: config.env.ELASTICSEARCH_PASSWORD,
asSuperuser: true,
});
const data = await runFleetServerIfNeeded();
fleetServerContainerId = data?.fleetServerContainerId;
});
on('after:run', () => {
if (fleetServerContainerId) {
execa.sync('docker', ['kill', fleetServerContainerId]);
}
});
on('task', {
createEndpointHost: async (
options: Omit<CreateAndEnrollEndpointHostOptions, 'log' | 'kbnClient'>
@ -224,7 +247,7 @@ export const dataLoadersForRealEndpoints = (
log,
kbnClient,
}).then((newHost) => {
return waitForEndpointToStreamData(kbnClient, newHost.agentId, 120000).then(() => {
return waitForEndpointToStreamData(kbnClient, newHost.agentId, 360000).then(() => {
return newHost;
});
});
@ -246,7 +269,15 @@ export const dataLoadersForRealEndpoints = (
path: string;
content: string;
}): Promise<null> => {
await execa(`multipass`, ['exec', hostname, '--', 'sh', '-c', `echo ${content} > ${path}`]);
if (process.env.CI) {
await execa('vagrant', ['ssh', '--', `echo ${content} > ${path}`], {
env: {
VAGRANT_CWD,
},
});
} else {
await execa(`multipass`, ['exec', hostname, '--', 'sh', '-c', `echo ${content} > ${path}`]);
}
return null;
},
@ -259,7 +290,16 @@ export const dataLoadersForRealEndpoints = (
srcPath: string;
destPath: string;
}): Promise<null> => {
await execa(`multipass`, ['transfer', srcPath, `${hostname}:${destPath}`]);
if (process.env.CI) {
await execa('vagrant', ['upload', srcPath, destPath], {
env: {
VAGRANT_CWD,
},
});
} else {
await execa(`multipass`, ['transfer', srcPath, `${hostname}:${destPath}`]);
}
return null;
},
@ -290,26 +330,37 @@ export const dataLoadersForRealEndpoints = (
path: string;
password?: string;
}): Promise<string> => {
const result = await execa(`multipass`, [
'exec',
hostname,
'--',
'sh',
'-c',
`unzip -p ${password ? `-P ${password} ` : ''}${path}`,
]);
let result;
if (process.env.CI) {
result = await execa(
`vagrant`,
['ssh', '--', `unzip -p ${password ? `-P ${password} ` : ''}${path}`],
{
env: {
VAGRANT_CWD,
},
}
);
} else {
result = await execa(`multipass`, [
'exec',
hostname,
'--',
'sh',
'-c',
`unzip -p ${password ? `-P ${password} ` : ''}${path}`,
]);
}
return result.stdout;
},
stopEndpointHost: async () => {
const hosts = await getEndpointHosts();
const hostName = hosts[0].name;
stopEndpointHost: async (hostName) => {
return stopEndpointHost(hostName);
},
startEndpointHost: async () => {
const hosts = await getEndpointHosts();
const hostName = hosts[0].name;
startEndpointHost: async (hostName) => {
return startEndpointHost(hostName);
},
});

View file

@ -16,7 +16,7 @@ export const cleanupRule = (id: string) => {
request({ method: 'DELETE', url: `/api/detection_engine/rules?id=${id}` });
};
export const loadRule = (includeResponseActions = true) =>
export const loadRule = (body = {}, includeResponseActions = true) =>
request<RuleResponse>({
method: 'POST',
url: `/api/detection_engine/rules`,
@ -55,6 +55,7 @@ export const loadRule = (includeResponseActions = true) =>
actions: [],
enabled: true,
throttle: 'no_actions',
...body,
...(includeResponseActions
? {
response_actions: [

View file

@ -5,20 +5,29 @@
* 2.0.
*/
export const ENDPOINT_VM_NAME = 'ENDPOINT_VM_NAME';
export const API_AUTH = Object.freeze({
user: Cypress.env('ELASTICSEARCH_USERNAME'),
pass: Cypress.env('ELASTICSEARCH_PASSWORD'),
});
export const API_HEADERS = Object.freeze({ 'kbn-xsrf': 'cypress' });
export const COMMON_API_HEADERS = { 'kbn-xsrf': 'cypress' };
export const request = <T = unknown>(
options: Partial<Cypress.RequestOptions>
): Cypress.Chainable<Cypress.Response<T>> =>
export const waitForPageToBeLoaded = () => {
cy.getByTestSubj('globalLoadingIndicator-hidden').should('exist');
cy.getByTestSubj('globalLoadingIndicator').should('not.exist');
};
export const loadPage = (url: string, options: Partial<Cypress.VisitOptions> = {}) => {
cy.visit(url, options);
waitForPageToBeLoaded();
};
export const request = <T = unknown>({
headers,
...options
}: Partial<Cypress.RequestOptions>): Cypress.Chainable<Cypress.Response<T>> =>
cy.request<T>({
auth: API_AUTH,
headers: API_HEADERS,
headers: Object.freeze({ ...COMMON_API_HEADERS, ...headers }),
...options,
});

View file

@ -17,6 +17,6 @@ export const createEndpointHost = (
{
agentPolicyId,
},
{ timeout: timeout ?? 180000 }
{ timeout: timeout ?? 600000 }
);
};

View file

@ -6,6 +6,7 @@
*/
import type { ActionDetails } from '../../../../common/endpoint/types';
import { loadPage } from './common';
const API_ENDPOINT_ACTION_PATH = '/api/endpoint/action/*';
export const interceptActionRequests = (
@ -70,9 +71,10 @@ export const waitForReleaseOption = (alertId: string): void => {
};
export const visitRuleAlerts = (ruleName: string) => {
cy.visit('/app/security/rules');
loadPage('/app/security/rules');
cy.contains(ruleName).click();
};
export const checkFlyoutEndpointIsolation = (): void => {
cy.getByTestSubj('event-field-agent.status').then(($status) => {
if ($status.find('[title="Isolated"]').length > 0) {
@ -90,7 +92,7 @@ export const checkFlyoutEndpointIsolation = (): void => {
};
export const toggleRuleOffAndOn = (ruleName: string): void => {
cy.visit('/app/security/rules');
loadPage('/app/security/rules');
cy.wait(2000);
cy.contains(ruleName)
.parents('tr')
@ -105,7 +107,8 @@ export const toggleRuleOffAndOn = (ruleName: string): void => {
export const filterOutEndpoints = (endpointHostname: string): void => {
cy.getByTestSubj('filters-global-container').within(() => {
cy.getByTestSubj('queryInput').click().type(`host.hostname : "${endpointHostname}"`);
cy.getByTestSubj('queryInput').click();
cy.getByTestSubj('queryInput').type(`host.name: ${endpointHostname}`);
cy.getByTestSubj('querySubmitButton').click();
});
};

View file

@ -13,7 +13,7 @@ import Url from 'url';
import type { Role } from '@kbn/security-plugin/common';
import { getWithResponseActionsRole } from '../../../../scripts/endpoint/common/roles_users/with_response_actions_role';
import { getNoResponseActionsRole } from '../../../../scripts/endpoint/common/roles_users/without_response_actions_role';
import { request } from './common';
import { request, loadPage } from './common';
import { getT1Analyst } from '../../../../scripts/endpoint/common/roles_users/t1_analyst';
import { getT2Analyst } from '../../../../scripts/endpoint/common/roles_users/t2_analyst';
import { getHunter } from '../../../../scripts/endpoint/common/roles_users/hunter';
@ -342,7 +342,7 @@ export const getEnvAuth = (): User => {
*/
export const loginAndWaitForPage = (url: string) => {
login();
cy.visit(url);
loadPage(url);
};
export const getRoleWithArtifactReadPrivilege = (privilegePrefix: string) => {

View file

@ -6,12 +6,13 @@
*/
import { TOGGLE_NAVIGATION_BTN } from '../screens/navigation';
import { loadPage } from './common';
export const INTEGRATIONS = 'app/integrations#/';
export const FLEET = 'app/fleet/';
export const FLEET_AGENT_POLICIES = 'app/fleet/policies';
export const navigateTo = (page: string, opts?: Partial<Cypress.VisitOptions>) => {
cy.visit(page, opts);
loadPage(page, opts);
cy.contains('Loading Elastic').should('exist');
cy.contains('Loading Elastic').should('not.exist');

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { request } from './common';
import { request, loadPage } from './common';
import { resolvePathVariables } from '../../../common/utils/resolve_path_variables';
import { ACTION_DETAILS_ROUTE } from '../../../../common/endpoint/constants';
import type { ActionDetails, ActionDetailsApiResponse } from '../../../../common/endpoint/types';
@ -32,7 +32,7 @@ export const focusAndOpenCommandDropdown = (number = 0) => {
});
};
export const fillUpNewRule = (name = 'Test', description = 'Test') => {
cy.visit('app/security/rules/management');
loadPage('app/security/rules/management');
cy.getByTestSubj('create-new-rule').click();
cy.getByTestSubj('stepDefineRule').within(() => {
cy.getByTestSubj('queryInput').first().type('_id:*{enter}');
@ -48,7 +48,7 @@ export const fillUpNewRule = (name = 'Test', description = 'Test') => {
cy.getByTestSubj('schedule-continue').click();
};
export const visitRuleActions = (ruleId: string) => {
cy.visit(`app/security/rules/id/${ruleId}/edit`);
loadPage(`app/security/rules/id/${ruleId}/edit`);
cy.getByTestSubj('edit-rule-actions-tab').should('exist');
cy.getByTestSubj('globalLoadingIndicator').should('not.exist');
cy.getByTestSubj('stepPanelProgress').should('not.exist');
@ -74,7 +74,7 @@ export const tryAddingDisabledResponseAction = (itemNumber = 0) => {
*/
export const waitForActionToComplete = (
actionId: string,
timeout = 60000
timeout = 120000
): Cypress.Chainable<ActionDetails> => {
let action: ActionDetails | undefined;

View file

@ -8,10 +8,11 @@
import type { ConsoleResponseActionCommands } from '../../../../common/endpoint/service/response_actions/constants';
import { closeAllToasts } from './toasts';
import { APP_ENDPOINTS_PATH } from '../../../../common/constants';
import { loadPage } from './common';
import Chainable = Cypress.Chainable;
export const waitForEndpointListPageToBeLoaded = (endpointHostname: string): void => {
cy.visit(APP_ENDPOINTS_PATH);
loadPage(APP_ENDPOINTS_PATH);
closeAllToasts();
cy.contains(endpointHostname).should('exist');
};

View file

@ -37,6 +37,8 @@ export default defineCypressConfig({
},
e2e: {
experimentalMemoryManagement: true,
experimentalInteractiveRunEvents: true,
baseUrl: 'http://localhost:5620',
supportFile: 'public/management/cypress/support/e2e.ts',
specPattern: 'public/management/cypress/e2e/endpoint/*.cy.{js,jsx,ts,tsx}',

View file

@ -20,6 +20,8 @@ import {
waitForHostToEnroll,
} from './fleet_services';
export const VAGRANT_CWD = `${__dirname}/../endpoint_agent_runner/`;
export interface CreateAndEnrollEndpointHostOptions
extends Pick<CreateMultipassVmOptions, 'disk' | 'cpus' | 'memory'> {
kbnClient: KbnClient;
@ -60,38 +62,48 @@ export const createAndEnrollEndpointHost = async ({
deleted: [],
});
const [vm, agentDownload, fleetServerUrl, enrollmentToken] = await Promise.all([
createMultipassVm({
vmName: hostname ?? `test-host-${Math.random().toString().substring(2, 6)}`,
disk,
cpus,
memory,
}),
const vmName = hostname ?? `test-host-${Math.random().toString().substring(2, 6)}`;
getAgentDownloadUrl(version, useClosestVersionMatch, log).then<{
url: string;
cache?: DownloadedAgentInfo;
}>((url) => {
if (useCache) {
cacheCleanupPromise = cleanupDownloads();
const agentDownload = await getAgentDownloadUrl(version, useClosestVersionMatch, log).then<{
url: string;
cache?: DownloadedAgentInfo;
}>((url) => {
if (useCache) {
cacheCleanupPromise = cleanupDownloads();
return downloadAndStoreAgent(url).then((cache) => {
return {
url,
cache,
};
});
}
return downloadAndStoreAgent(url).then((cache) => {
return {
url,
cache,
};
});
}
return { url };
}),
return { url };
});
const [vm, fleetServerUrl, enrollmentToken] = await Promise.all([
process.env.CI
? createVagrantVm({
vmName,
log,
cachedAgentDownload: agentDownload.cache as DownloadedAgentInfo,
})
: createMultipassVm({
vmName,
disk,
cpus,
memory,
}),
fetchFleetServerUrl(kbnClient),
fetchAgentPolicyEnrollmentKey(kbnClient, agentPolicyId),
]);
log.verbose(await execa('multipass', ['info', vm.vmName]));
if (!process.env.CI) {
log.verbose(await execa('multipass', ['info', vm.vmName]));
}
// Some validations before we proceed
assert(agentDownload.url, 'Missing agent download URL');
@ -143,6 +155,54 @@ export const destroyEndpointHost = async (
]);
};
interface CreateVmResponse {
vmName: string;
}
interface CreateVagrantVmOptions {
vmName: string;
cachedAgentDownload: DownloadedAgentInfo;
log: ToolingLog;
}
/**
* Creates a new VM using `vagrant`
*/
const createVagrantVm = async ({
vmName,
cachedAgentDownload,
log,
}: CreateVagrantVmOptions): Promise<CreateVmResponse> => {
try {
await execa.command(`vagrant destroy -f`, {
env: {
VAGRANT_CWD,
},
});
// eslint-disable-next-line no-empty
} catch (e) {}
try {
await execa.command(`vagrant up`, {
env: {
VAGRANT_DISABLE_VBOXSYMLINKCREATE: '1',
VAGRANT_CWD,
VMNAME: vmName,
CACHED_AGENT_SOURCE: cachedAgentDownload.fullFilePath,
CACHED_AGENT_FILENAME: cachedAgentDownload.filename,
},
stdio: ['inherit', 'inherit', 'inherit'],
});
} catch (e) {
log.error(e);
throw e;
}
return {
vmName,
};
};
interface CreateMultipassVmOptions {
vmName: string;
/** Number of CPUs */
@ -153,10 +213,6 @@ interface CreateMultipassVmOptions {
memory?: string;
}
interface CreateMultipassVmResponse {
vmName: string;
}
/**
* Creates a new VM using `multipass`
*/
@ -165,7 +221,7 @@ const createMultipassVm = async ({
disk = '8G',
cpus = 1,
memory = '1G',
}: CreateMultipassVmOptions): Promise<CreateMultipassVmResponse> => {
}: CreateMultipassVmOptions): Promise<CreateVmResponse> => {
await execa.command(
`multipass launch --name ${vmName} --disk ${disk} --cpus ${cpus} --memory ${memory}`
);
@ -176,7 +232,15 @@ const createMultipassVm = async ({
};
const deleteMultipassVm = async (vmName: string): Promise<void> => {
await execa.command(`multipass delete -p ${vmName}`);
if (process.env.CI) {
await execa.command(`vagrant destroy -f`, {
env: {
VAGRANT_CWD,
},
});
} else {
await execa.command(`multipass delete -p ${vmName}`);
}
};
interface EnrollHostWithFleetOptions {
@ -206,35 +270,30 @@ const enrollHostWithFleet = async ({
`Installing agent on host using cached download from [${cachedAgentDownload.fullFilePath}]`
);
// mount local folder on VM
await execa.command(
`multipass mount ${cachedAgentDownload.directory} ${vmName}:~/_agent_downloads`
);
await execa.command(
`multipass exec ${vmName} -- tar -zxf _agent_downloads/${cachedAgentDownload.filename}`
);
await execa.command(`multipass unmount ${vmName}:~/_agent_downloads`);
if (!process.env.CI) {
// mount local folder on VM
await execa.command(
`multipass mount ${cachedAgentDownload.directory} ${vmName}:~/_agent_downloads`
);
await execa.command(
`multipass exec ${vmName} -- tar -zxf _agent_downloads/${cachedAgentDownload.filename}`
);
await execa.command(`multipass unmount ${vmName}:~/_agent_downloads`);
}
} else {
log.verbose(`downloading and installing agent from URL [${agentDownloadUrl}]`);
// download into VM
await execa.command(
`multipass exec ${vmName} -- curl -L ${agentDownloadUrl} -o ${agentDownloadedFile}`
);
await execa.command(`multipass exec ${vmName} -- tar -zxf ${agentDownloadedFile}`);
await execa.command(`multipass exec ${vmName} -- rm -f ${agentDownloadedFile}`);
if (!process.env.CI) {
// download into VM
await execa.command(
`multipass exec ${vmName} -- curl -L ${agentDownloadUrl} -o ${agentDownloadedFile}`
);
await execa.command(`multipass exec ${vmName} -- tar -zxf ${agentDownloadedFile}`);
await execa.command(`multipass exec ${vmName} -- rm -f ${agentDownloadedFile}`);
}
}
const agentInstallArguments = [
'exec',
vmName,
'--working-directory',
`/home/ubuntu/${vmDirName}`,
'--',
'sudo',
'./elastic-agent',
@ -253,10 +312,28 @@ const enrollHostWithFleet = async ({
];
log.info(`Enrolling elastic agent with Fleet`);
log.verbose(`Command: multipass ${agentInstallArguments.join(' ')}`);
if (process.env.CI) {
log.verbose(`Command: vagrant ${agentInstallArguments.join(' ')}`);
await execa(`multipass`, agentInstallArguments);
await execa(`vagrant`, ['ssh', '--', `cd ${vmDirName} && ${agentInstallArguments.join(' ')}`], {
env: {
VAGRANT_CWD,
},
stdio: ['inherit', 'inherit', 'inherit'],
});
} else {
log.verbose(`Command: multipass ${agentInstallArguments.join(' ')}`);
await execa(`multipass`, [
'exec',
vmName,
'--working-directory',
`/home/ubuntu/${vmDirName}`,
'--',
...agentInstallArguments,
]);
}
log.info(`Waiting for Agent to check-in with Fleet`);
const agent = await waitForHostToEnroll(kbnClient, vmName, 120000);
@ -273,9 +350,24 @@ export async function getEndpointHosts(): Promise<
}
export function stopEndpointHost(hostName: string) {
if (process.env.CI) {
return execa('vagrant', ['suspend'], {
env: {
VAGRANT_CWD,
VMNAME: hostName,
},
});
}
return execa('multipass', ['stop', hostName]);
}
export function startEndpointHost(hostName: string) {
if (process.env.CI) {
return execa('vagrant', ['up'], {
env: {
VAGRANT_CWD,
},
});
}
return execa('multipass', ['start', hostName]);
}

View file

@ -0,0 +1,29 @@
hostname = ENV["VMNAME"] || 'ubuntu'
cachedAgentSource = ENV["CACHED_AGENT_SOURCE"] || ''
cachedAgentFilename = ENV["CACHED_AGENT_FILENAME"] || ''
Vagrant.configure("2") do |config|
config.vm.hostname = hostname
config.vm.box = 'ubuntu/jammy64'
config.vm.provider :virtualbox do |vb|
vb.memory = 4096
vb.cpus = 2
end
config.vm.provider :vmware_desktop do |v, override|
override.vm.box = "starboard/ubuntu-arm64-20.04.5"
override.vm.box_version = "20221120.20.40.0"
override.vm.box_download_insecure = true
override.vm.network "private_network", type: "dhcp"
v.ssh_info_public = true
v.gui = true
v.linked_clone = false
v.vmx["ethernet0.virtualdev"] = "vmxnet3"
end
config.vm.provision "file", source: cachedAgentSource, destination: "~/#{cachedAgentFilename}"
config.vm.provision "shell", inline: "tar -zxf #{cachedAgentFilename} && rm -f #{cachedAgentFilename}"
config.vm.provision "shell", inline: "sudo apt-get install unzip"
end

View file

@ -60,13 +60,23 @@ export const enrollEndpointHost = async (): Promise<string | undefined> => {
disk: '8G',
});
log.info(`VM created using Multipass.
VM Name: ${vmName}
Elastic Agent Version: ${version}
if (process.env.CI) {
log.info(`VM created using Vagrant.
VM Name: ${vmName}
Elastic Agent Version: ${version}
Shell access: ${chalk.bold(`multipass shell ${vmName}`)}
Delete VM: ${chalk.bold(`multipass delete -p ${vmName}${await getVmCountNotice()}`)}
`);
Shell access: ${chalk.bold(`vagrant ssh ${vmName}`)}
Delete VM: ${chalk.bold(`vagrant destroy ${vmName} -f`)}
`);
} else {
log.info(`VM created using Multipass.
VM Name: ${vmName}
Elastic Agent Version: ${version}
Shell access: ${chalk.bold(`multipass shell ${vmName}`)}
Delete VM: ${chalk.bold(`multipass delete -p ${vmName}${await getVmCountNotice()}`)}
`);
}
} catch (error) {
log.error(dump(error));
log.indent(-4);

View file

@ -50,22 +50,11 @@ export const runFleetServerIfNeeded = async (): Promise<
const {
log,
kibana: { isLocalhost: isKibanaOnLocalhost },
kbnClient,
} = getRuntimeServices();
log.info(`Setting up fleet server (if necessary)`);
log.indent(4);
const currentFleetServerUrl = await fetchFleetServerUrl(kbnClient);
if (currentFleetServerUrl) {
log.info(
`Fleet server is already enrolled with Fleet - URL:\n${currentFleetServerUrl}\nNothing to do.`
);
log.indent(-4);
return;
}
try {
fleetServerAgentPolicyId = await getOrCreateFleetServerAgentPolicyId();
const serviceToken = await generateFleetServiceToken();

View file

@ -37,6 +37,7 @@ export const startRuntimeServices = async ({
username,
password,
log,
asSuperuser: otherOptions?.asSuperuser,
});
runtimeServices = {

View file

@ -16,4 +16,5 @@ export interface StartRuntimeServicesOptions {
version?: string;
policy?: string;
log?: ToolingLog;
asSuperuser?: boolean;
}

View file

@ -99,6 +99,10 @@ export const cli = () => {
};
const getKibanaPort = <T>(): T | number => {
if (isOpen) {
return 5620;
}
const kibanaPort = parseInt(`56${Math.floor(Math.random() * 89) + 10}`, 10);
if (kibanaPorts.includes(kibanaPort)) {
return getKibanaPort();
@ -108,6 +112,10 @@ export const cli = () => {
};
const getFleetServerPort = <T>(): T | number => {
if (isOpen) {
return 8220;
}
const fleetServerPort = parseInt(`82${Math.floor(Math.random() * 89) + 10}`, 10);
if (fleetServerPorts.includes(fleetServerPort)) {
return getFleetServerPort();
@ -378,7 +386,7 @@ export const cli = () => {
}
await procs.stop('kibana');
shutdownEs();
await shutdownEs();
cleanupServerPorts({ esPort, kibanaPort, fleetServerPort });
return result;

View file

@ -1,37 +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 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();
return this.vmName;
}
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

@ -49,7 +49,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
'servers.elasticsearch.port'
)}`,
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
'endpointRbacEnabled',
'endpointResponseActionsEnabled',
])}`,
],

View file

@ -9,7 +9,7 @@ import { getLocalhostRealIp } from '@kbn/security-solution-plugin/scripts/endpoi
import { FtrConfigProviderContext } from '@kbn/test';
import { ExperimentalFeatures } from '@kbn/security-solution-plugin/common/experimental_features';
import { DefendWorkflowsCypressEndpointTestRunner } from './runner';
import { DefendWorkflowsCypressCliTestRunner } from './runner';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const defendWorkflowsCypressConfig = await readConfigFile(require.resolve('./config.ts'));
@ -43,6 +43,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
`--xpack.securitySolution.enableExperimental=${JSON.stringify(enabledFeatureFlags)}`,
],
},
testRunner: DefendWorkflowsCypressEndpointTestRunner,
testRunner: DefendWorkflowsCypressCliTestRunner,
};
}

View file

@ -1,43 +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 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> {
const fleetServerConfig = await runFleetServerIfNeeded();
if (!fleetServerConfig) {
throw new Error('Fleet server config not found');
}
this.fleetContainerId = fleetServerConfig.fleetServerContainerId;
}
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

@ -1,15 +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.
*/
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

@ -6,72 +6,9 @@
*/
import Url from 'url';
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';
type RunnerEnv = Record<string, string | undefined>;
async function withFleetAgent(
{ getService }: FtrProviderContext,
runner: (runnerEnv: RunnerEnv) => RunnerEnv
) {
const log = getService('log');
const config = getService('config');
const kbnClient = getService('kibanaServer');
const elasticUrl = Url.format(config.get('servers.elasticsearch'));
const kibanaUrl = Url.format(config.get('servers.kibana'));
const fleetServerUrl = config.get('servers.fleetserver')
? Url.format(config.get('servers.fleetserver'))
: undefined;
const username = config.get('servers.elasticsearch.username');
const password = config.get('servers.elasticsearch.password');
await startRuntimeServices({
log,
elasticUrl,
kibanaUrl,
fleetServerUrl,
username,
password,
version: await getLatestAvailableAgentVersion(kbnClient),
});
const fleetManager = new FleetManager(log);
const agentManager = new AgentManager(log);
await fleetManager.setup();
const agentVmName = await agentManager.setup();
try {
await runner({ agentVmName });
} finally {
agentManager.cleanup();
fleetManager.cleanup();
}
}
export async function DefendWorkflowsCypressCliTestRunner(context: FtrProviderContext) {
return startDefendWorkflowsCypress(context, 'dw:run');
}
export async function DefendWorkflowsCypressVisualTestRunner(context: FtrProviderContext) {
return startDefendWorkflowsCypress(context, 'dw:open');
}
export async function DefendWorkflowsCypressEndpointTestRunner(context: FtrProviderContext) {
return withFleetAgent(context, (runnerEnv) =>
startDefendWorkflowsCypress(context, 'dw:endpoint:open', runnerEnv)
);
}
function startDefendWorkflowsCypress(
context: FtrProviderContext,
cypressCommand: 'dw:endpoint:open' | 'dw:open' | 'dw:run',
runnerEnv?: RunnerEnv
) {
export function DefendWorkflowsCypressCliTestRunner(context: FtrProviderContext) {
const config = context.getService('config');
return {
@ -87,6 +24,5 @@ function startDefendWorkflowsCypress(
hostname: config.get('servers.kibana.hostname'),
port: config.get('servers.kibana.port'),
}),
ENDPOINT_VM_NAME: runnerEnv?.agentVmName,
};
}

View file

@ -1,39 +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 axios from 'axios';
import semver from 'semver';
import { map } 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) => map(response.data.versions, (version) => version.split('-SNAPSHOT')[0]));
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;
};

View file

@ -1,19 +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 { FtrConfigProviderContext } from '@kbn/test';
import { DefendWorkflowsCypressVisualTestRunner } from './runner';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const defendWorkflowsCypressConfig = await readConfigFile(require.resolve('./config.ts'));
return {
...defendWorkflowsCypressConfig.getAll(),
testRunner: DefendWorkflowsCypressVisualTestRunner,
};
}

View file

@ -12,7 +12,7 @@ import { FtrProviderContext } from './ftr_provider_context';
import { AgentManager } from './agent';
import { FleetManager } from './fleet_server';
import { getLatestAvailableAgentVersion } from '../defend_workflows_cypress/utils';
import { getLatestAvailableAgentVersion } from './utils';
async function setupFleetAgent({ getService }: FtrProviderContext) {
const log = getService('log');

View file

@ -5,6 +5,9 @@
* 2.0.
*/
import axios from 'axios';
import semver from 'semver';
import { map } from 'lodash';
import { PackagePolicy, CreatePackagePolicyResponse } from '@kbn/fleet-plugin/common';
import { KbnClient } from '@kbn/test';
@ -48,3 +51,31 @@ export const addIntegrationToAgentPolicy = async (
},
});
};
/**
* 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) => map(response.data.versions, (version) => version.split('-SNAPSHOT')[0]));
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;
};