Skip to content

Commit

Permalink
CCS Cypress integration (elastic#103941) (elastic#104496)
Browse files Browse the repository at this point in the history
* Add CCS Cypress test runner

* Split flow for CCS Cypress tests

* Make esArchiver load data onto the remote cluster

* Add CCS specific rules with customizable remote name

* Allow overriding @kbn/dev-utils's CA_CERT_PATH

* Add CCS related docs

Co-authored-by: Gloria Hornero <[email protected]>

Co-authored-by: Domenico Andreoli <[email protected]>
Co-authored-by: Gloria Hornero <[email protected]>
  • Loading branch information
3 people authored Jul 6, 2021
1 parent b85bf3c commit b1335ea
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 1 deletion.
2 changes: 1 addition & 1 deletion packages/kbn-dev-utils/src/certs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import { resolve } from 'path';

export const CA_CERT_PATH = resolve(__dirname, '../certs/ca.crt');
export const CA_CERT_PATH = process.env.TEST_CA_CERT_PATH || resolve(__dirname, '../certs/ca.crt');
export const ES_KEY_PATH = resolve(__dirname, '../certs/elasticsearch.key');
export const ES_CERT_PATH = resolve(__dirname, '../certs/elasticsearch.crt');
export const ES_P12_PATH = resolve(__dirname, '../certs/elasticsearch.p12');
Expand Down
72 changes: 72 additions & 0 deletions x-pack/plugins/security_solution/cypress/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,42 @@ cd x-pack/plugins/security_solution
CYPRESS_BASE_URL=http(s)://<username>:<password>@<kbnUrl> CYPRESS_ELASTICSEARCH_URL=http(s)://<username>:<password>@<elsUrl> CYPRESS_ELASTICSEARCH_USERNAME=<username> CYPRESS_ELASTICSEARCH_PASSWORD=password yarn cypress:run:firefox
```

#### CCS Custom Target + Headless

This test execution requires two clusters configured for CCS. See [Search across clusters](https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-cross-cluster-search.html) for instructions on how to prepare such setup.

The instructions below assume:
* Search cluster is on server1
* Remote cluster is on server2
* Remote cluster is accessible from the search cluster with name `remote`
* Security and TLS are enabled

```shell
# bootstrap Kibana from the project root
yarn kbn bootstrap

# launch the Cypress test runner with overridden environment variables
cd x-pack/plugins/security_solution
CYPRESS_ELASTICSEARCH_USERNAME="user" \
CYPRESS_ELASTICSEARCH_PASSWORD="pass" \
CYPRESS_BASE_URL="https://user:pass@server1:5601" \
CYPRESS_ELASTICSEARCH_URL="https://user:pass@server1:9200" \
CYPRESS_CCS_KIBANA_URL="https://user:pass@server2:5601" \
CYPRESS_CCS_ELASTICSEARCH_URL="https://user:pass@server2:9200" \
CYPRESS_CCS_REMOTE_NAME="remote" \
yarn cypress:run:ccs
```

Similar sequence, just ending with `yarn cypress:open:ccs`, can be used for interactive test running via Cypress UI.

Appending `--browser firefox` to the `yarn cypress:run:ccs` command above will run the tests on Firefox instead of Chrome.

## Folder Structure

### ccs_integration/

Contains the specs that are executed in a Cross Cluster Search configuration, typically during integration tests.

### integration/

Cypress convention. Contains the specs that are going to be executed.
Expand Down Expand Up @@ -208,6 +242,44 @@ Because of `cy.exec`, used to invoke `es_archiver`, it's necessary to override i

> Warning: Setting the NODE_TLS_REJECT_UNAUTHORIZED environment variable to '0' makes TLS connections and HTTPS requests insecure by disabling certificate verification.
### CCS

Tests running in CCS configuration need to care about two aspects:

1. data (eg. to trigger alerts) is generated/loaded on the remote cluster
2. queries (eg. detection rules) refer to remote indices

Incorrect handling of the above points might result in false positives, in that the remote cluster is not involved but the test passes anyway.

#### Remote data loading

Helpers `esArchiverCCSLoad` and `esArchiverCCSUnload` are provided by [cypress/tasks/es_archiver.ts](https://github.com/elastic/kibana/blob/master/x-pack/plugins/security_solution/cypress/tasks/es_archiver.ts):

```javascript
import { esArchiverCCSLoad, esArchiverCCSUnload } from '../../tasks/es_archiver';
```

They will use the `CYPRESS_CCS_*_URL` environment variables for accessing the remote cluster. Complex tests involving local and remote data can interleave them with `esArchiverLoad` and `esArchiverUnload` as needed.

#### Remote indices queries

Queries accessing remote indices follow the usual `<remote_name>:<remote_index>` notation but should not hard-code the remote name in the test itself.

For such reason the environemnt variable `CYPRESS_CCS_REMOTE_NAME` is defined and, in the case of detection rules, used as shown below:

```javascript
const ccsRemoteName: string = Cypress.env('CCS_REMOTE_NAME');

export const unmappedCCSRule: CustomRule = {
customQuery: '*:*',
index: [`${ccsRemoteName}:unmapped*`],
...
};

```

Similar approach should be used in defining all index patterns, rules, and queries to be applied on remote data.

## Development Best Practices

### Clean up the state
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* 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 { CELL_TEXT, JSON_LINES, TABLE_ROWS } from '../../screens/alerts_details';

import {
expandFirstAlert,
waitForAlertsIndexToBeCreated,
waitForAlertsPanelToBeLoaded,
} from '../../tasks/alerts';
import { openJsonView, openTable, scrollJsonViewToBottom } from '../../tasks/alerts_details';
import { createCustomRuleActivated } from '../../tasks/api_calls/rules';
import { cleanKibana } from '../../tasks/common';
import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { esArchiverCCSLoad, esArchiverCCSUnload } from '../../tasks/es_archiver';

import { unmappedCCSRule } from '../../objects/rule';

import { ALERTS_URL } from '../../urls/navigation';

describe('Alert details with unmapped fields', () => {
beforeEach(() => {
cleanKibana();
esArchiverCCSLoad('unmapped_fields');
loginAndWaitForPageWithoutDateRange(ALERTS_URL);
waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
createCustomRuleActivated(unmappedCCSRule);
loginAndWaitForPageWithoutDateRange(ALERTS_URL);
waitForAlertsPanelToBeLoaded();
expandFirstAlert();
});

afterEach(() => {
esArchiverCCSUnload('unmapped_fields');
});

it('Displays the unmapped field on the JSON view', () => {
const expectedUnmappedField = { line: 2, text: ' "unmapped": "This is the unmapped field"' };

openJsonView();
scrollJsonViewToBottom();

cy.get(JSON_LINES).then((elements) => {
const length = elements.length;
cy.wrap(elements)
.eq(length - expectedUnmappedField.line)
.should('have.text', expectedUnmappedField.text);
});
});

it('Displays the unmapped field on the table', () => {
const expectedUnmmappedField = {
row: 55,
field: 'unmapped',
text: 'This is the unmapped field',
};

openTable();

cy.get(TABLE_ROWS)
.eq(expectedUnmmappedField.row)
.within(() => {
cy.get(CELL_TEXT).eq(0).should('have.text', expectedUnmmappedField.field);
cy.get(CELL_TEXT).eq(1).should('have.text', expectedUnmmappedField.text);
});
});
});
20 changes: 20 additions & 0 deletions x-pack/plugins/security_solution/cypress/objects/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export const totalNumberOfPrebuiltRulesInEsArchive = 127;

export const totalNumberOfPrebuiltRulesInEsArchiveCustomRule = 145;

const ccsRemoteName: string = Cypress.env('CCS_REMOTE_NAME');

interface MitreAttackTechnique {
name: string;
subtechniques: string[];
Expand Down Expand Up @@ -198,6 +200,24 @@ export const unmappedRule: CustomRule = {
maxSignals: 100,
};

export const unmappedCCSRule: CustomRule = {
customQuery: '*:*',
index: [`${ccsRemoteName}:unmapped*`],
name: 'Rule with unmapped fields',
description: 'The new rule description.',
severity: 'High',
riskScore: '17',
tags: ['test', 'newRule'],
referenceUrls: ['http://example.com/', 'https://example.com/'],
falsePositivesExamples: ['False1', 'False2'],
mitre: [mitre1, mitre2],
note: '# test markdown',
runsEvery,
lookBack,
timeline,
maxSignals: 100,
};

export const existingRule: CustomRule = {
customQuery: 'host.name: *',
name: 'Rule 1',
Expand Down
18 changes: 18 additions & 0 deletions x-pack/plugins/security_solution/cypress/tasks/es_archiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const ES_ARCHIVE_DIR = '../../test/security_solution_cypress/es_archives';
const CONFIG_PATH = '../../test/functional/config.js';
const ES_URL = Cypress.env('ELASTICSEARCH_URL');
const KIBANA_URL = Cypress.config().baseUrl;
const CCS_ES_URL = Cypress.env('CCS_ELASTICSEARCH_URL');
const CCS_KIBANA_URL = Cypress.env('CCS_KIBANA_URL');

// Otherwise cy.exec would inject NODE_TLS_REJECT_UNAUTHORIZED=0 and node would abort if used over https
const NODE_TLS_REJECT_UNAUTHORIZED = '1';
Expand All @@ -37,3 +39,19 @@ export const esArchiverResetKibana = () => {
{ env: { NODE_TLS_REJECT_UNAUTHORIZED }, failOnNonZeroExit: false }
);
};

export const esArchiverCCSLoad = (folder: string) => {
const path = Path.join(ES_ARCHIVE_DIR, folder);
cy.exec(
`node ../../../scripts/es_archiver load "${path}" --config "${CONFIG_PATH}" --es-url "${CCS_ES_URL}" --kibana-url "${CCS_KIBANA_URL}"`,
{ env: { NODE_TLS_REJECT_UNAUTHORIZED } }
);
};

export const esArchiverCCSUnload = (folder: string) => {
const path = Path.join(ES_ARCHIVE_DIR, folder);
cy.exec(
`node ../../../scripts/es_archiver unload "${path}" --config "${CONFIG_PATH}" --es-url "${CCS_ES_URL}" --kibana-url "${CCS_KIBANA_URL}"`,
{ env: { NODE_TLS_REJECT_UNAUTHORIZED } }
);
};
2 changes: 2 additions & 0 deletions x-pack/plugins/security_solution/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
"build-beat-doc": "node scripts/beat_docs/build.js && node ../../../scripts/eslint ./server/utils/beat_schema/fields.ts --fix",
"cypress": "../../../node_modules/.bin/cypress",
"cypress:open": "yarn cypress open --config-file ./cypress/cypress.json",
"cypress:open:ccs": "yarn cypress:open --config integrationFolder=./cypress/ccs_integration",
"cypress:open-as-ci": "node ../../../scripts/functional_tests --config ../../test/security_solution_cypress/visual_config.ts",
"cypress:run": "yarn cypress:run:reporter --browser chrome --headless --spec './cypress/integration/**/*.spec.ts'; status=$?; yarn junit:merge && exit $status",
"cypress:run:firefox": "yarn cypress:run:reporter --browser firefox --headless --spec './cypress/integration/**/*.spec.ts'; status=$?; yarn junit:merge && exit $status",
"cypress:run:reporter": "yarn cypress run --config-file ./cypress/cypress.json --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json",
"cypress:run:ccs": "yarn cypress:run:reporter --browser chrome --headless --config integrationFolder=./cypress/ccs_integration",
"cypress:run-as-ci": "node --max-old-space-size=2048 ../../../scripts/functional_tests --config ../../test/security_solution_cypress/cli_config.ts",
"cypress:run-as-ci:firefox": "node --max-old-space-size=2048 ../../../scripts/functional_tests --config ../../test/security_solution_cypress/config.firefox.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/",
Expand Down
19 changes: 19 additions & 0 deletions x-pack/test/security_solution_cypress/ccs_config.ts
Original file line number Diff line number Diff line change
@@ -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 { FtrConfigProviderContext } from '@kbn/test';

import { SecuritySolutionCypressCcsTestRunner } from './runner';

export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const securitySolutionCypressConfig = await readConfigFile(require.resolve('./config.ts'));
return {
...securitySolutionCypressConfig.getAll(),

testRunner: SecuritySolutionCypressCcsTestRunner,
};
}
24 changes: 24 additions & 0 deletions x-pack/test/security_solution_cypress/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,30 @@ export async function SecuritySolutionCypressCliFirefoxTestRunner({
});
}

export async function SecuritySolutionCypressCcsTestRunner({ getService }: FtrProviderContext) {
const log = getService('log');

await withProcRunner(log, async (procs) => {
await procs.run('cypress', {
cmd: 'yarn',
args: ['cypress:run:ccs'],
cwd: resolve(__dirname, '../../plugins/security_solution'),
env: {
FORCE_COLOR: '1',
CYPRESS_BASE_URL: process.env.TEST_KIBANA_URL,
CYPRESS_ELASTICSEARCH_URL: process.env.TEST_ES_URL,
CYPRESS_ELASTICSEARCH_USERNAME: process.env.ELASTICSEARCH_USERNAME,
CYPRESS_ELASTICSEARCH_PASSWORD: process.env.ELASTICSEARCH_PASSWORD,
CYPRESS_CCS_KIBANA_URL: process.env.TEST_KIBANA_URLDATA,
CYPRESS_CCS_ELASTICSEARCH_URL: process.env.TEST_ES_URLDATA,
CYPRESS_CCS_REMOTE_NAME: process.env.TEST_CCS_REMOTE_NAME,
...process.env,
},
wait: true,
});
});
}

export async function SecuritySolutionCypressVisualTestRunner({ getService }: FtrProviderContext) {
const log = getService('log');
const config = getService('config');
Expand Down

0 comments on commit b1335ea

Please sign in to comment.