[ON-WEEK][POC] Playwright (#190803)

## UPDATE
It has been removed the execution of the playwright tests on buildkite,
the execution will be re-enabled as soon as we are ready and as
described below in the PR, there are still steps pending to be done.

## Motivation

**Cypress is not performing well lately.**
* We have been facing significant performance issues with Cypress. For
instance, it takes a long time to open the visual interface and start
executing tests.

**Teams are finding it increasingly challenging to write new tests and
debug existing ones.**
* The time and effort required to create new tests or troubleshoot
existing ones have become burdensome.

**Concern about the impact this could have on our testing practices.**
* Lose motivation to write tests or, worse, skip writing crucial tests.

## Why Playwright?

* Compared to Cypress, Playwright seems to be known for its faster
execution times and lower resource consumption. What could have a
positive impact by having faster feedback during development and
execution of new tests as well as more efficient use of CI resources.

* Provides powerful debugging tools which can make easier to write,
debug and execute tests.

* Seems to provide the same capabilities we currently use in our Cypress
tests.

* Given Playwright's active development and backing by Microsoft, it is
likely to continue evolving rapidly, making it a safe long-term choice.

Considering all the above, Playwright seems to be a strong candidate to
replace Cypress and address all the issues we are facing lately
regarding UI test automation.

## Objective of this POC

To write in Playwright a couple of tests we currently have on Cypress to
check the performance of the tool as well as the development experience.

The tests selected have been:
-
[enable_risk_score_redirect.cy.ts](https://github.com/elastic/kibana/blob/main/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/dashboards/enable_risk_score_redirect.cy.ts)
- Owned by Entity Analytics team and selected by its simplicity since it
does not need any special setup to be executed and is short.

-
[manual_rule_run.cy.ts](https://github.com/elastic/kibana/blob/main/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_gaps/manual_rule_run.cy.ts)
- Owned by Detection Engine team and selected because is short and adds
a bit more of complexity due to it needs of clean-up and setting up
initial data through the API.
  
## How to execute the tests

### Visual mode
- Navigate to: `x-pack/test/security_solution_playwright`
- Execute: `yarn open:ess` for ESS environment or `yarn open:serverless`
for serverless environment.

### Headless mode
- Navigate to: `x-pack/test/security_solution_playwright`
- Execute: `yarn run:ess` for ESS environment or `yarn run:serverless`
for serverless environment.

### From VScode
- Install `Playwright Test for VScode` extension by Microsoft
- Navigate to: `x-pack/test/security_solution_playwright`
- Execute: `yarn open:ess` for ESS environment or `yarn open:serverless`
for serverless environment.
- Open your IDE
- Click on the `Testing` icon
- On the `Test Explorer` click on the three dots to select the profile
you are going to execute `ess` or `serverless`
- Click on the test you want to execute or navigate to the spec file of
the test and execute it from the same spec.

## My experience
- Tests are way easier to implement than with Cypress.
- Playwright does not rely on chainable commands. Chainable commands on
Cypress can lead to confusing code.
- Without chainable commands, the flow of the tests is more explicit and
easier to understand.
- You can notice that the tool has been designed with Typescript in
mind.
- Is super easy to implement the Page Object Model pattern (POM).
- With POM the test code is clean and focused on "what" rather than
"how".
- Love the fact that you can execute the tests from the same IDE without
having to switch windows during test development.
- The visual mode execution gives you lots of information out of the
box.

## The scope of this PR
- Sets the initial infrastructure to write and execute tests with
Playwright.
- Has examples and set a basis about how to write tests using the POM.
- Allows the execution of the tests in ESS and serverless (just
stateless environment).
- Integrates the execution of the tests with buildkite.

## Pending to be done/investigate
- Proper readme
- How to split tests and PO between the different teams
- Good reports on CI
- Upload screenshots on CI
- Flaky test suite runner 
- Complete the labeling
- Execution of the tests on MKI environments

## FAQ
**Can I start adding tests to playwright?**
Currently, you can explore and experiment with Playwright, but there is
still work pending to be done to make the tool officially usable.

**Why security engineering productivity is the owner of the playwright
folder?**
This is something temporary to make sure that good practices are
followed.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: dkirchan <diamantis.kirchantzoglou@elastic.co>
Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com>
Co-authored-by: Jon <jon@budzenski.me>
This commit is contained in:
Gloria Hornero 2024-09-06 13:09:18 +02:00 committed by GitHub
parent 7ad92ba86f
commit 4041d274b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 9280 additions and 14 deletions

View file

@ -12,6 +12,9 @@ disabled:
- x-pack/test_serverless/functional/test_suites/security/cypress/security_config.ts
- x-pack/test/security_solution_cypress/serverless_config.ts
# Playwright
- x-pack/test/security_solution_playwright/serverless_config.ts
# Serverless base config files
- x-pack/test_serverless/api_integration/config.base.ts
- x-pack/test_serverless/functional/config.base.ts

View file

@ -24,6 +24,9 @@ disabled:
- x-pack/test/threat_intelligence_cypress/cli_config_parallel.ts
- x-pack/test/threat_intelligence_cypress/config.ts
# Playwright
- x-pack/test/security_solution_playwright/playwright.config.ts
defaultQueue: 'n2-4-spot'
enabled:
- x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/actions/trial_license_complete_tier/configs/ess.config.ts

View file

@ -0,0 +1,30 @@
steps:
- command: .buildkite/scripts/steps/functional/security_serverless_playwright.sh
label: 'Serverless Playwright - Security Solution Tests'
agents:
machineType: n2-standard-4
preemptible: true
depends_on:
- build
- quick_checks
timeout_in_minutes: 60
parallelism: 1
retry:
automatic:
- exit_status: '-1'
limit: 1
- command: .buildkite/scripts/steps/functional/security_solution_playwright.sh
label: 'Playwright - Security Solution Tests'
agents:
machineType: n2-standard-4
preemptible: true
depends_on:
- build
- quick_checks
timeout_in_minutes: 60
parallelism: 1
retry:
automatic:
- exit_status: '-1'
limit: 1

View file

@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
source .buildkite/scripts/steps/functional/common.sh
export JOB=kibana-security-solution-playwright
export KIBANA_INSTALL_DIR=${KIBANA_BUILD_LOCATION}
echo "--- SERVERLESS - Security Solution Playwright Tests"
cd x-pack/test/security_solution_playwright
yarn run:serverless; exit_code=$?; exit $exit_code

View file

@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
source .buildkite/scripts/steps/functional/common.sh
export JOB=kibana-security-solution-playwright
export KIBANA_INSTALL_DIR=${KIBANA_BUILD_LOCATION}
echo "--- ESS - Security Solution Playwright Tests"
cd x-pack/test/security_solution_playwright
yarn run:ess; exit_code=$?; exit $exit_code

1
.github/CODEOWNERS vendored
View file

@ -1481,6 +1481,7 @@ x-pack/test/security_solution_api_integration/test_suites/sources @elastic/secur
/x-pack/test/security_solution_cypress/cypress/* @elastic/security-engineering-productivity
/x-pack/test/security_solution_cypress/cypress/tasks/login.ts @elastic/security-engineering-productivity
/x-pack/test/security_solution_cypress/es_archives @elastic/security-engineering-productivity
/x-pack/test/security_solution_playwright @elastic/security-engineering-productivity
/x-pack/plugins/security_solution/scripts/run_cypress @MadameSheema @patrykkopycinski @maximpn @banderror
## Security Solution sub teams - Threat Hunting Investigations

8
.gitignore vendored
View file

@ -150,3 +150,11 @@ oas_docs/output/kibana.serverless.tmp*.yaml
oas_docs/output/kibana.tmp*.yaml
oas_docs/output/kibana.new.yaml
oas_docs/output/kibana.serverless.new.yaml
# Security Solution Playwright
x-pack/test/security_solution_playwright/test-results/
x-pack/test/security_solution_playwright/playwright-report/
x-pack/test/security_solution_playwright/blob-report/
x-pack/test/security_solution_playwright/playwright/.cache/
x-pack/test/security_solution_playwright/.auth/
x-pack/test/security_solution_playwright/.env

View file

@ -152,6 +152,10 @@ export async function installYarnDeps(log, opts = undefined) {
});
log.success('yarn deps installed');
await run('yarn', ['playwright', 'install']);
log.success('Playwright browsers installed');
}
/**

View file

@ -1072,6 +1072,7 @@
"deepmerge": "^4.2.2",
"del": "^6.1.0",
"diff": "^5.1.0",
"dotenv": "^16.4.5",
"elastic-apm-node": "^4.7.3",
"email-addresses": "^5.0.0",
"eventsource-parser": "^1.1.1",
@ -1462,6 +1463,7 @@
"@mapbox/vector-tile": "1.3.1",
"@octokit/rest": "^17.11.2",
"@parcel/watcher": "^2.1.0",
"@playwright/test": "=1.46.0",
"@redocly/cli": "^1.21.0",
"@statoscope/webpack-plugin": "^5.28.2",
"@storybook/addon-a11y": "^6.5.16",
@ -1761,7 +1763,8 @@
"pirates": "^4.0.1",
"piscina": "^3.2.0",
"pixelmatch": "^5.3.0",
"playwright": "=1.38.0",
"playwright": "=1.46.0",
"playwright-chromium": "=1.46.0",
"pngjs": "^3.4.0",
"postcss": "^8.4.31",
"postcss-loader": "^4.2.0",

View file

@ -485,6 +485,15 @@
"matchBaseBranches": ["main"],
"labels": ["release_note:skip", "team:obs-entities"],
"enabled": true
},
{
"groupName": "Security Engineering Productivity",
"matchDepNames": ["dotenv", "playwright-chromium", "@playwright/test"],
"reviewers": ["team:security-engineering-productivity"],
"matchBaseBranches": ["main"],
"labels": ["Team: Sec Eng Productivity", "release_note:skip", "backport:all-open"],
"minimumReleaseAge": "7 days",
"enabled": true
}
],
"customManagers": [

View file

@ -0,0 +1,377 @@
/*
* 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 { run } from '@kbn/dev-cli-runner';
import yargs from 'yargs';
import _ from 'lodash';
import globby from 'globby';
import pMap from 'p-map';
import { withProcRunner } from '@kbn/dev-proc-runner';
import path from 'path';
import fs from 'fs';
import { EsVersion, FunctionalTestRunner, runElasticsearch, runKibanaServer } from '@kbn/test';
import {
Lifecycle,
ProviderCollection,
readProviderSpec,
} from '@kbn/test/src/functional_test_runner/lib';
import pRetry from 'p-retry';
import execa from 'execa';
import { prefixedOutputLogger } from '../endpoint/common/utils';
import { createToolingLogger } from '../../common/endpoint/data_loaders/utils';
import { parseTestFileConfig, retrieveIntegrations } from '../run_cypress/utils';
import { getFTRConfig } from '../run_cypress/get_ftr_config';
import type { StartedFleetServer } from '../endpoint/common/fleet_server/fleet_server_services';
import { startFleetServer } from '../endpoint/common/fleet_server/fleet_server_services';
import { createKbnClient } from '../endpoint/common/stack_services';
export const cli = () => {
run(
async ({ log: _cliLogger }) => {
const { argv } = yargs(process.argv.slice(2))
.coerce('configFile', (arg) => (_.isArray(arg) ? _.last(arg) : arg))
.coerce('spec', (arg) => (_.isArray(arg) ? _.last(arg) : arg))
.coerce('env', (arg: string) =>
arg.split(',').reduce((acc, curr) => {
const [key, value] = curr.split('=');
if (key === 'burn') {
acc[key] = parseInt(value, 10);
} else {
acc[key] = value;
}
return acc;
}, {} as Record<string, string | number>)
)
.boolean('inspect');
_cliLogger.info(`
----------------------------------------------
Script arguments:
----------------------------------------------
${JSON.stringify(argv, null, 2)}
----------------------------------------------
`);
const isOpen = argv._.includes('open');
const playwrightConfigFilePath = require.resolve(`../../${argv.configFile}`) as string;
const playwrightConfigFile = await import(playwrightConfigFilePath);
const log = prefixedOutputLogger('playwright', createToolingLogger());
log.info(`
----------------------------------------------
Playwright config for file: ${playwrightConfigFilePath}:
----------------------------------------------
${JSON.stringify(playwrightConfigFile, null, 2)}
----------------------------------------------
`);
const specConfig = playwrightConfigFile.testMatch;
const specArg = argv.spec;
const specPattern = specArg ?? specConfig;
const files = retrieveIntegrations(globby.sync(specPattern));
const esPorts: number[] = [9200, 9220];
const kibanaPorts: number[] = [5601, 5620];
const fleetServerPorts: number[] = [8220];
const getEsPort = <T>(): T | number => {
if (isOpen) {
return 9220;
}
const esPort = parseInt(`92${Math.floor(Math.random() * 89) + 10}`, 10);
if (esPorts.includes(esPort)) {
return getEsPort();
}
esPorts.push(esPort);
return esPort;
};
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();
}
kibanaPorts.push(kibanaPort);
return kibanaPort;
};
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();
}
fleetServerPorts.push(fleetServerPort);
return fleetServerPort;
};
const cleanupServerPorts = ({
esPort,
kibanaPort,
fleetServerPort,
}: {
esPort: number;
kibanaPort: number;
fleetServerPort: number;
}) => {
_.pull(esPorts, esPort);
_.pull(kibanaPorts, kibanaPort);
_.pull(fleetServerPorts, fleetServerPort);
};
const failedSpecFilePaths: string[] = [];
const runSpecs = async (filePaths: string[]) =>
pMap(
filePaths,
async (filePath) => {
let result: Error | undefined;
await withProcRunner(log, async (procs) => {
const abortCtrl = new AbortController();
const onEarlyExit = (msg: string) => {
log.error(msg);
abortCtrl.abort();
};
const esPort: number = getEsPort();
const kibanaPort: number = getKibanaPort();
const fleetServerPort: number = getFleetServerPort();
const specFileFTRConfig = parseTestFileConfig(filePath);
const ftrConfigFilePath = path.resolve(
_.isArray(argv.ftrConfigFile) ? _.last(argv.ftrConfigFile) : argv.ftrConfigFile
);
const config = await getFTRConfig({
log,
esPort,
kibanaPort,
fleetServerPort,
ftrConfigFilePath,
specFilePath: filePath,
specFileFTRConfig,
isOpen,
});
const createUrlFromFtrConfig = (
type: 'elasticsearch' | 'kibana' | 'fleetserver',
withAuth: boolean = false
): string => {
const getKeyPath = (keyPath: string = ''): string => {
return `servers.${type}${keyPath ? `.${keyPath}` : ''}`;
};
if (!config.get(getKeyPath())) {
throw new Error(`Unable to create URL for ${type}. Not found in FTR config at `);
}
const url = new URL('http://localhost');
url.port = config.get(getKeyPath('port'));
url.protocol = config.get(getKeyPath('protocol'));
url.hostname = config.get(getKeyPath('hostname'));
if (withAuth) {
url.username = config.get(getKeyPath('username'));
url.password = config.get(getKeyPath('password'));
}
return url.toString().replace(/\/$/, '');
};
const baseUrl = createUrlFromFtrConfig('kibana');
const lifecycle = new Lifecycle(log);
const providers = new ProviderCollection(log, [
...readProviderSpec('Service', {
lifecycle: () => lifecycle,
log: () => log,
config: () => config,
}),
...readProviderSpec('Service', config.get('services')),
]);
const options = {
installDir: process.env.KIBANA_INSTALL_DIR,
ci: process.env.CI,
};
let fleetServer: StartedFleetServer | undefined;
let shutdownEs;
try {
shutdownEs = await pRetry(
async () =>
runElasticsearch({
config,
log,
name: `ftr-${esPort}`,
esFrom: config.get('esTestCluster')?.from || 'snapshot',
onEarlyExit,
}),
{ retries: 2, forever: false }
);
await runKibanaServer({
procs,
config,
installDir: options?.installDir,
extraKbnOpts:
options?.installDir || options?.ci || !isOpen
? []
: ['--dev', '--no-dev-credentials'],
onEarlyExit,
inspect: argv.inspect,
});
if (playwrightConfigFile.env?.WITH_FLEET_SERVER) {
log.info(`Setting up fleet-server for this Cypress config`);
const kbnClient = createKbnClient({
url: baseUrl,
username: config.get('servers.kibana.username'),
password: config.get('servers.kibana.password'),
log,
});
fleetServer = await pRetry(
async () =>
startFleetServer({
kbnClient,
logger: log,
port:
fleetServerPort ?? config.has('servers.fleetserver.port')
? (config.get('servers.fleetserver.port') as number)
: undefined,
// `force` is needed to ensure that any currently running fleet server (perhaps left
// over from an interrupted run) is killed and a new one restarted
force: true,
}),
{ retries: 2, forever: false }
);
}
await providers.loadAll();
const functionalTestRunner = new FunctionalTestRunner(
log,
config,
EsVersion.getDefault()
);
const ftrEnv = await pRetry(() => functionalTestRunner.run(abortCtrl.signal), {
retries: 1,
});
// Normalized the set of available env vars in Playwright
const playwrightCustomEnv = {
...ftrEnv,
// NOTE:
// ELASTICSEARCH_URL needs to be created here with auth because SIEM Playwright setup depends on it. At some
// points we should probably try to refactor that code to use `ELASTICSEARCH_URL_WITH_AUTH` instead
ELASTICSEARCH_URL:
ftrEnv.ELASTICSEARCH_URL ?? createUrlFromFtrConfig('elasticsearch', true),
ELASTICSEARCH_URL_WITH_AUTH: createUrlFromFtrConfig('elasticsearch', true),
ELASTICSEARCH_USERNAME:
ftrEnv.ELASTICSEARCH_USERNAME ?? config.get('servers.elasticsearch.username'),
ELASTICSEARCH_PASSWORD:
ftrEnv.ELASTICSEARCH_PASSWORD ?? config.get('servers.elasticsearch.password'),
FLEET_SERVER_URL: createUrlFromFtrConfig('fleetserver'),
KIBANA_URL: baseUrl,
KIBANA_URL_WITH_AUTH: createUrlFromFtrConfig('kibana', true),
KIBANA_USERNAME: config.get('servers.kibana.username'),
KIBANA_PASSWORD: config.get('servers.kibana.password'),
IS_SERVERLESS: config.get('serverless'),
};
const envFilePath = path.resolve(
__dirname,
'..',
'..',
'..',
'..',
'test',
'security_solution_playwright',
'.env'
);
const envContent = Object.entries(playwrightCustomEnv)
.map(([key, value]) => `${key}=${value}`)
.join('\n');
fs.writeFileSync(envFilePath, envContent);
log.info(`
----------------------------------------------
Playwright run ENV for file: ${filePath}
----------------------------------------------
`);
const project = playwrightCustomEnv.IS_SERVERLESS ? 'serverless' : 'ess';
if (isOpen) {
await execa.command(
`../../../node_modules/.bin/playwright test --config ${playwrightConfigFilePath} --ui --project ${project}`,
{
env: {
...playwrightCustomEnv,
},
stdout: process.stdout,
}
);
} else {
await execa.command(
`../../../node_modules/.bin/playwright test --config ${playwrightConfigFilePath} --project ${project} --grep @${project}`,
{
env: {
...playwrightCustomEnv,
FILE_PATH: filePath,
},
stdout: process.stdout,
}
);
}
} catch (error) {
log.error(error);
result = error;
failedSpecFilePaths.push(filePath);
}
if (fleetServer) {
await fleetServer.stop();
}
await procs.stop('kibana');
await shutdownEs?.();
cleanupServerPorts({ esPort, kibanaPort, fleetServerPort });
return result;
});
return result;
},
{
concurrency: 1,
}
);
await runSpecs(files);
},
{
flags: {
allowUnexpected: true,
},
}
);
};

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
require('../../../../../src/setup_node_env');
require('./playwright').cli();

View file

@ -19,7 +19,6 @@ import {
FtrConfigProviderContext,
} from '@kbn/test';
import path from 'path';
// @ts-expect-error we have to check types with "allowJs: false" for now, causing this import to fail
import { REPO_ROOT } from '@kbn/repo-info';
import { STATEFUL_ROLES_ROOT_PATH } from '@kbn/es';
import { DeploymentAgnosticCommonServices, services } from '../services';

View file

@ -7,7 +7,6 @@
import { FtrConfigProviderContext, findTestPluginPaths } from '@kbn/test';
import { resolve } from 'path';
// @ts-expect-error we have to check types with "allowJs: false" for now, causing this import to fail
import { REPO_ROOT as KIBANA_ROOT } from '@kbn/repo-info';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {

View file

@ -6,7 +6,6 @@
*/
import expect from '@kbn/expect';
// @ts-expect-error we have to check types with "allowJs: false" for now, causing this import to fail
import { REPO_ROOT } from '@kbn/repo-info';
import fs from 'fs';
import path from 'path';

View file

@ -7,7 +7,6 @@
import { FtrConfigProviderContext, findTestPluginPaths } from '@kbn/test';
import { resolve } from 'path';
// @ts-expect-error we have to check types with "allowJs: false" for now, causing this import to fail
import { REPO_ROOT as KIBANA_ROOT } from '@kbn/repo-info';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {

View file

@ -6,7 +6,6 @@
*/
import path from 'path';
// @ts-expect-error we have to check types with "allowJs: false" for now, causing this import to fail
import { REPO_ROOT as KIBANA_ROOT } from '@kbn/repo-info';
import { FtrConfigProviderContext } from '@kbn/test';

View file

@ -7,7 +7,6 @@
import { withProcRunner } from '@kbn/dev-proc-runner';
import { resolve } from 'path';
// @ts-expect-error we have to check types with "allowJs: false" for now, causing this import to fail
import { REPO_ROOT } from '@kbn/repo-info';
import Fs from 'fs';
import { createFlagError } from '@kbn/dev-cli-errors';

View file

@ -6,7 +6,6 @@
*/
import { resolve } from 'path';
// @ts-expect-error we have to check types with "allowJs: false" for now, causing this import to fail
import { REPO_ROOT as KIBANA_ROOT } from '@kbn/repo-info';
import { FtrConfigProviderContext, findTestPluginPaths } from '@kbn/test';
import { services } from './services';

View file

@ -6,8 +6,6 @@
*/
import path from 'path';
// @ts-expect-error we have to check types with "allowJs: false" for now, causing this import to fail
import { REPO_ROOT } from '@kbn/repo-info';
import { FtrConfigProviderContext } from '@kbn/test';

View file

@ -8,7 +8,6 @@
import { FtrConfigProviderContext, getKibanaCliLoggers } from '@kbn/test';
import fs from 'fs';
import path from 'path';
// @ts-expect-error we have to check types with "allowJs: false" for now, causing this import to fail
import { REPO_ROOT } from '@kbn/repo-info';
import { createFlagError } from '@kbn/dev-cli-errors';
import { v4 as uuidV4 } from 'uuid';

View file

@ -55,6 +55,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
// packages listed in fleet_packages.json
// See: https://elastic.slack.com/archives/CNMNXV4RG/p1683033379063079
`--xpack.fleet.developer.bundledPackageLocation=./inexistentDir`,
'--csp.strict=false',
'--csp.warnLegacyBrowsers=false',
],
},
};

View file

@ -11,8 +11,8 @@ import { AllConnectorsResponse } from '@kbn/actions-plugin/common/routes/connect
import { DETECTION_ENGINE_RULES_BULK_ACTION } from '@kbn/security-solution-plugin/common/constants';
import { ELASTICSEARCH_PASSWORD, ELASTICSEARCH_USERNAME } from '../../env_var_names_constants';
import { deleteAllDocuments } from './elasticsearch';
import { DEFAULT_ALERTS_INDEX_PATTERN } from './alerts';
import { getSpaceUrl } from '../space';
import { DEFAULT_ALERTS_INDEX_PATTERN } from './alerts';
export const API_AUTH = Object.freeze({
user: Cypress.env(ELASTICSEARCH_USERNAME),

View file

@ -35,6 +35,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
{ product_line: 'cloud', product_tier: 'complete' },
])}`,
`--xpack.securitySolution.enableExperimental=${JSON.stringify(['manualRuleRunEnabled'])}`,
'--csp.strict=false',
'--csp.warnLegacyBrowsers=false',
],
},
testRunner: SecuritySolutionConfigurableCypressTestRunner,

View file

@ -0,0 +1,84 @@
/*
* 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 fs from 'fs';
import yaml from 'js-yaml';
import { ToolingLog } from '@kbn/tooling-log';
import { HostOptions, SamlSessionManager } from '@kbn/test';
import { resolve } from 'path';
import { REPO_ROOT } from '@kbn/repo-info';
const getYamlData = (filePath: string): any => {
const fileContents = fs.readFileSync(filePath, 'utf8');
return yaml.safeLoad(fileContents);
};
const getRoleConfiguration = (role: string, filePath: string): any => {
const data = getYamlData(filePath);
if (data[role]) {
return data[role];
} else {
throw new Error(`Role '${role}' not found in the YAML file.`);
}
};
const rolesPath =
'../../../packages/kbn-es/src/serverless_resources/project_roles/security/roles.yml';
export const getApiKeyForUser = async () => {
const log = new ToolingLog({ level: 'verbose', writeTo: process.stdout });
const kbnHost = process.env.KIBANA_URL || process.env.BASE_URL;
const kbnUrl = new URL(kbnHost!);
const hostOptions: HostOptions = {
protocol: kbnUrl.protocol as 'http' | 'https',
hostname: kbnUrl.hostname,
port: parseInt(kbnUrl.port, 10),
username: process.env.ELASTICSEARCH_USERNAME ?? '',
password: process.env.ELASTICSEARCH_PASSWORD ?? '',
};
const rolesFilename = process.env.PROXY_ORG ? `${process.env.PROXY_ORG}.json` : undefined;
const cloudUsersFilePath = resolve(REPO_ROOT, '.ftr', rolesFilename ?? 'role_users.json');
const samlSessionManager = new SamlSessionManager({
hostOptions,
log,
isCloud: process.env.CLOUD_SERVERLESS === 'true',
cloudUsersFilePath,
});
const adminCookieHeader = await samlSessionManager.getApiCredentialsForRole('admin');
let roleDescriptor = {};
const roleConfig = getRoleConfiguration('admin', rolesPath);
roleDescriptor = { ['system_indices_superuser']: roleConfig };
const response = await axios.post(
`${process.env.KIBANA_URL}/internal/security/api_key`,
{
name: 'myTestApiKey',
metadata: {},
role_descriptors: roleDescriptor,
},
{
headers: {
'kbn-xsrf': 'cypress-creds',
'x-elastic-internal-origin': 'security-solution',
...adminCookieHeader,
},
}
);
const apiKey = response.data.encoded;
return apiKey;
};

View file

@ -0,0 +1,30 @@
/*
* 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 { APIRequestContext } from '@playwright/test';
import { getCommonHeaders } from './headers';
export const deleteAllSecurityDocuments = async (request: APIRequestContext) => {
const securityIndexes = `.lists-*,.items-*,.alerts-security.alerts-*`;
const headers = await getCommonHeaders();
await request.post(`${process.env.ELASTICSEARCH_URL}/${securityIndexes}/_refresh`, {
headers,
});
await request.post(
`${process.env.ELASTICSEARCH_URL}/${securityIndexes}/_delete_by_query?conflicts=proceed&scroll_size=10000&refresh`,
{
headers,
data: {
query: {
match_all: {},
},
},
}
);
};

View file

@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { INITIAL_REST_VERSION } from '@kbn/data-views-plugin/server/constants';
import { getApiKeyForUser } from './api_key';
export const getCommonHeaders = async (additionalHeaders: Record<string, string> = {}) => {
let auth = '';
if (process.env.IS_SERVERLESS === 'true') {
const apiKey = await getApiKeyForUser();
auth = `ApiKey ${apiKey}`;
} else {
const username = process.env.ELASTICSEARCH_USERNAME || '';
const password = process.env.ELASTICSEARCH_PASSWORD || '';
const encodedCredentials = Buffer.from(`${username}:${password}`).toString('base64');
auth = `Basic ${encodedCredentials}`;
}
return {
'kbn-xsrf': 'cypress-creds',
'x-elastic-internal-origin': 'security-solution',
Authorization: auth,
...additionalHeaders,
};
};
export const getCommonHeadersWithApiVersion = async () => {
const header = await getCommonHeaders({
'elastic-api-version': INITIAL_REST_VERSION,
});
return header;
};

View file

@ -0,0 +1,50 @@
/*
* 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 {
DETECTION_ENGINE_RULES_BULK_ACTION,
DETECTION_ENGINE_RULES_URL,
} from '@kbn/security-solution-plugin/common/constants';
import { APIRequestContext } from '@playwright/test';
import { getRuleForAlertTesting } from '../../common/utils/security_solution';
import { getCommonHeadersWithApiVersion } from './headers';
const indexes = [
'apm-*-transaction*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'logs-*',
'packetbeat-*',
'traces-apm*',
'winlogbeat-*',
'-*elastic-cloud-logs-*',
];
export const createRule = async (request: APIRequestContext) => {
const data = getRuleForAlertTesting(indexes);
const headers = await getCommonHeadersWithApiVersion();
const response = await request.post(DETECTION_ENGINE_RULES_URL, {
data,
headers,
});
return await response.json();
};
export const deleteAllRules = async (request: APIRequestContext) => {
const headers = await getCommonHeadersWithApiVersion();
const response = await request.post(DETECTION_ENGINE_RULES_BULK_ACTION, {
data: {
query: '',
action: 'delete',
},
headers,
});
return await response.json();
};

View file

@ -0,0 +1,123 @@
{
"type": "doc",
"value": {
"id": "_aZE5nwBOpWiDweSth_E",
"index": "auditbeat-2022",
"source": {
"@timestamp" : "2022-03-04T19:41:34.045Z",
"host" : {
"hostname" : "test.local",
"architecture" : "x86_64",
"os" : {
"platform" : "darwin",
"version" : "10.16",
"family" : "darwin",
"name" : "Mac OS X",
"kernel" : "21.3.0",
"build" : "21D62",
"type" : "macos"
},
"id" : "44426D67-79AB-547C-7777-440AB8F5DDD2",
"ip" : [
"fe80::bade:48ff:fe00:1122",
"fe81::4ab:9565:1199:be3",
"192.168.5.175",
"fe80::40d7:d0ff:fe66:f55",
"fe81::40d8:d0ff:fe66:f55",
"fe82::c2c:6bdf:3307:dce0",
"fe83::5069:fcd5:e31c:7059",
"fe80::ce81:b2c:bd2c:69e",
"fe80::febc:bbc1:c517:827b",
"fe80::6d09:bee6:55a5:539d",
"fe80::c920:752e:1e0e:edc9",
"fe80::a4a:ca38:761f:83e2"
],
"mac" : [
"ad:df:48:00:11:22",
"a6:86:e7:ae:5a:b6",
"a9:83:e7:ae:5a:b6",
"43:d8:d0:66:0f:55",
"42:d8:d0:66:0f:57",
"82:70:c7:c2:3c:01",
"82:70:c6:c2:4c:00",
"82:76:a6:c2:3c:05",
"82:70:c6:b2:3c:04",
"82:71:a6:c2:3c:01"
],
"name" : "siem-kibana"
},
"agent" : {
"type" : "winlogbeat",
"version" : "8.1.0",
"ephemeral_id" : "f6df090f-656a-4a79-a6a1-0c8671c9752d",
"id" : "0ebd469b-c164-4734-00e6-96d018098dc7",
"name" : "test.local"
},
"event" : {
"module" : "sysmon",
"dataset" : "process",
"kind" : "event",
"category" : [
"process"
],
"type" : [
"start"
],
"action" : "process_started"
},
"destination": {
"port": 80
},
"process" : {
"start" : "2022-03-04T19:41:34.902Z",
"pid" : 30884,
"working_directory" : "/Users/test/security_solution",
"hash" : {
"sha1" : "ae2d46c38fa207efbea5fcecd6294eebbf5af00f"
},
"parent" : {
"pid" : 777
},
"executable" : "/bin/zsh",
"name" : "zsh",
"args" : [
"-zsh",
"unique"
],
"entity_id" : "q6pltOhTWlQx3BCE",
"entry_leader": {
"entity_id": "q6pltOhTWlQx3BCE",
"name": "fake entry",
"pid": 2342342
}
},
"message" : "Process zsh (PID: 27884) by user test STARTED",
"user" : {
"id" : "505",
"group" : {
"name" : "staff",
"id" : "20"
},
"effective" : {
"id" : "505",
"group" : {
"id" : "20"
}
},
"saved" : {
"id" : "505",
"group" : {
"id" : "20"
}
},
"name" : "test"
},
"service" : {
"type" : "system"
},
"ecs" : {
"version" : "8.0.0"
}
}
}
}

View file

@ -0,0 +1,61 @@
/*
* 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 Fs from 'fs';
import * as Url from 'url';
import { EsArchiver } from '@kbn/es-archiver';
import { createEsClientForTesting, KbnClient, systemIndicesSuperuser } from '@kbn/test';
import { ToolingLog } from '@kbn/tooling-log';
import { CA_CERT_PATH } from '@kbn/dev-utils';
export const createEsArchiver = async () => {
const log = new ToolingLog({ level: 'verbose', writeTo: process.stdout });
const isServerless = process.env.IS_SERVERLESS === 'true';
const isCloudServerless = process.env.CLOUD_SERVERLESS === 'true';
const serverlessCloudUser = {
username: process.env.ELASTICSEARCH_USERNAME ?? '',
password: process.env.ELASTICSEARCH_PASSWORD ?? '',
};
if (isServerless && (!serverlessCloudUser.username || !serverlessCloudUser.password)) {
throw new Error(
'ELASTICSEARCH_USERNAME and ELASTICSEARCH_PASSWORD must be defined for serverless configuration'
);
}
let authOverride;
if (isServerless) {
authOverride = isCloudServerless ? serverlessCloudUser : systemIndicesSuperuser;
}
const esUrl = process.env.ELASTICSEARCH_URL;
if (!esUrl) {
throw new Error('ELASTICSEARCH_URL environment variable is not set');
}
const client = createEsClientForTesting({
esUrl: Url.format(new URL(esUrl)),
authOverride,
});
const kibanaUrl = process.env.KIBANA_URL || process.env.BASE_URL;
const kbnClient = new KbnClient({
log,
url: kibanaUrl as string,
...(esUrl.includes('https') ? { certificateAuthorities: [Fs.readFileSync(CA_CERT_PATH)] } : {}),
});
return new EsArchiver({
log,
client,
kbnClient,
baseDir: './es_archives',
});
};

View file

@ -0,0 +1,41 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { test as base } from '@playwright/test';
import { ToolingLog } from '@kbn/tooling-log';
import { HostOptions, SamlSessionManager } from '@kbn/test';
import { resolve } from 'path';
import { REPO_ROOT } from '@kbn/repo-info';
export const test = base.extend({
samlSessionManager: async ({}, use) => {
const log = new ToolingLog({ level: 'verbose', writeTo: process.stdout });
const kbnHost = process.env.KIBANA_URL || process.env.BASE_URL;
const kbnUrl = new URL(kbnHost!);
const hostOptions: HostOptions = {
protocol: kbnUrl.protocol as 'http' | 'https',
hostname: kbnUrl.hostname,
port: parseInt(kbnUrl.port, 10),
username: process.env.ELASTICSEARCH_USERNAME ?? '',
password: process.env.ELASTICSEARCH_PASSWORD ?? '',
};
const rolesFilename = process.env.PROXY_ORG ? `${process.env.PROXY_ORG}.json` : undefined;
const cloudUsersFilePath = resolve(REPO_ROOT, '.ftr', rolesFilename ?? 'role_users.json');
const samlSessionManager = new SamlSessionManager({
hostOptions,
log,
isCloud: process.env.CLOUD_SERVERLESS === 'true',
cloudUsersFilePath,
});
await use(samlSessionManager);
},
});

View file

@ -0,0 +1,13 @@
/*
* 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.
*/
declare module '@kbn/repo-info' {
export const REPO_ROOT: string;
}
declare global {
const ReadableStream: any;
}

View file

@ -0,0 +1,15 @@
{
"author": "Elastic",
"name": "security_solution_playwright",
"version": "1.0.0",
"private": true,
"license": "Elastic License 2.0",
"scripts": {
"playwright:open": "node ../../plugins/security_solution/scripts/run_playwright/start_playwright open --config-file ../../test/security_solution_playwright/playwright.config.ts",
"playwright:run": "node ../../plugins/security_solution/scripts/run_playwright/start_playwright run --config-file ../../test/security_solution_playwright/playwright.config.ts",
"open:ess": "npm run playwright:open -- --ftr-config-file ../security_solution_cypress/cli_config",
"run:ess": "npm run playwright:run -- --ftr-config-file ../security_solution_cypress/cli_config",
"open:serverless": "npm run playwright:open -- --ftr-config-file ../security_solution_cypress/serverless_config",
"run:serverless": "npm run playwright:run -- --ftr-config-file ../security_solution_cypress/serverless_config"
}
}

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { Locator, Page } from '@playwright/test';
const PAGE_TITLE = '[data-test-subj="entityAnalyticsManagementPageTitle"]';
export class EntityAnalyticsManagementPage {
page: Page;
entityAnalyticsManagementPageTitle!: Locator;
constructor(page: Page) {
this.page = page;
}
async initialize() {
this.entityAnalyticsManagementPageTitle = this.page.locator(PAGE_TITLE);
}
}

View file

@ -0,0 +1,51 @@
/*
* 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 { Locator, Page, expect } from '@playwright/test';
import { EntityAnalyticsManagementPage } from './entity_analytics_management_po';
import { PageFactory } from './page_factory';
const PAGE_URL = '/app/security/entity_analytics';
const ENABLE_HOST_RISK_SCORE_BUTTON = '[data-test-subj="enable_host_risk_score"]';
const ENABLE_USER_RISK_SCORE_BUTTON = '[data-test-subj="enable_user_risk_score"]';
export class EntityAnalyticsPage {
page: Page;
enableHostRiskScoreBtn!: Locator;
enableUserRiskScoreBtn!: Locator;
constructor(page: Page) {
this.page = page;
}
async initialize() {
this.enableHostRiskScoreBtn = this.page.locator(ENABLE_HOST_RISK_SCORE_BUTTON);
this.enableUserRiskScoreBtn = this.page.locator(ENABLE_USER_RISK_SCORE_BUTTON);
}
async navigates() {
await this.page.goto(PAGE_URL);
}
async enableHostRisk(): Promise<EntityAnalyticsManagementPage> {
await this.enableHostRiskScoreBtn.click();
return await PageFactory.createEntityAnalyticsManagementPage(this.page);
}
async enableUserRisk(): Promise<EntityAnalyticsManagementPage> {
await this.enableUserRiskScoreBtn.click();
return await PageFactory.createEntityAnalyticsManagementPage(this.page);
}
async waitForEnableHostRiskScoreToBePresent() {
await expect(this.enableHostRiskScoreBtn).toBeVisible();
}
async waitForEnableUserRiskScoreToBePresent() {
await expect(this.enableUserRiskScoreBtn).toBeVisible();
}
}

View file

@ -0,0 +1,40 @@
/*
* 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 { Page } from '@playwright/test';
import { EntityAnalyticsPage } from './entity_analytics_po';
import { EntityAnalyticsManagementPage } from './entity_analytics_management_po';
import { RuleDetailsPage } from './rule_details_page_po';
import { RuleManagementPage } from './rule_management_po';
export class PageFactory {
static async createEntityAnalyticsPage(page: Page): Promise<EntityAnalyticsPage> {
const entityAnalyticsPage = new EntityAnalyticsPage(page);
await entityAnalyticsPage.initialize();
return entityAnalyticsPage;
}
static async createEntityAnalyticsManagementPage(
page: Page
): Promise<EntityAnalyticsManagementPage> {
const entityAnalyticsManagementPage = new EntityAnalyticsManagementPage(page);
await entityAnalyticsManagementPage.initialize();
return entityAnalyticsManagementPage;
}
static async createRuleDetailsPage(page: Page): Promise<RuleDetailsPage> {
const ruleDetailsPage = new RuleDetailsPage(page);
await ruleDetailsPage.initialize();
return ruleDetailsPage;
}
static async createRuleManagementPage(page: Page): Promise<RuleManagementPage> {
const ruleManagementPage = new RuleManagementPage(page);
await ruleManagementPage.initialize();
return ruleManagementPage;
}
}

View file

@ -0,0 +1,44 @@
/*
* 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 { Locator, Page } from '@playwright/test';
const PAGE_URL = '/app/security/rules/id/';
const POPOVER_ACTIONS_TRIGGER_BUTTON = '[data-test-subj="rules-details-popover-button-icon"]';
const RULE_DETAILS_MANUAL_RULE_RUN_BTN = '[data-test-subj="rules-details-manual-rule-run"]';
const MODAL_CONFIRMATION_BTN = '[data-test-subj="confirmModalConfirmButton"]';
const TOASTER = '[data-test-subj="euiToastHeader"]';
export class RuleDetailsPage {
page: Page;
popoverActionsTriggerButton!: Locator;
ruleDetailsManualRuleRunButton!: Locator;
modalConfirmationBtn!: Locator;
toaster!: Locator;
constructor(page: Page) {
this.page = page;
}
async initialize() {
this.popoverActionsTriggerButton = this.page.locator(POPOVER_ACTIONS_TRIGGER_BUTTON);
this.ruleDetailsManualRuleRunButton = this.page.locator(RULE_DETAILS_MANUAL_RULE_RUN_BTN);
this.modalConfirmationBtn = this.page.locator(MODAL_CONFIRMATION_BTN);
this.toaster = this.page.locator(TOASTER);
}
async navigateTo(ruleId: string) {
await this.page.goto(`${PAGE_URL}${ruleId}`);
}
async manualRuleRun() {
await this.popoverActionsTriggerButton.click();
await this.ruleDetailsManualRuleRunButton.click();
await this.modalConfirmationBtn.click();
}
}

View file

@ -0,0 +1,56 @@
/*
* 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 { expect, Locator, Page } from '@playwright/test';
const PAGE_URL = '/app/security/rules/management';
const AUTO_REFRESH_POPOVER_TRIGGER_BUTTON = '[data-test-subj="autoRefreshButton"]';
const REFRESH_SETTINGS_SWITCH = '[data-test-subj="refreshSettingsSwitch"]';
const COLLAPSED_ACTION_BTN = '[data-test-subj="euiCollapsedItemActionsButton"]';
const MANUAL_RULE_RUN_ACTION_BTN = '[data-test-subj="manualRuleRunAction"]';
const MODAL_CONFIRMATION_BTN = '[data-test-subj="confirmModalConfirmButton"]';
const TOASTER = '[data-test-subj="euiToastHeader"]';
export class RuleManagementPage {
page: Page;
autoRefreshPopoverTriggerButton!: Locator;
refreshSettingsSwitch!: Locator;
collapsedActionBtn!: Locator;
manualRuleRunActionBtn!: Locator;
modalConfirmationBtn!: Locator;
toaster!: Locator;
constructor(page: Page) {
this.page = page;
}
async initialize() {
this.autoRefreshPopoverTriggerButton = this.page.locator(AUTO_REFRESH_POPOVER_TRIGGER_BUTTON);
this.refreshSettingsSwitch = this.page.locator(REFRESH_SETTINGS_SWITCH);
this.collapsedActionBtn = this.page.locator(COLLAPSED_ACTION_BTN);
this.manualRuleRunActionBtn = this.page.locator(MANUAL_RULE_RUN_ACTION_BTN);
this.modalConfirmationBtn = this.page.locator(MODAL_CONFIRMATION_BTN);
this.toaster = this.page.locator(TOASTER);
}
async navigate() {
await this.page.goto(PAGE_URL);
}
async disableAutoRefresh() {
await this.autoRefreshPopoverTriggerButton.click();
await this.refreshSettingsSwitch.click();
}
async manuallyRunFirstRule() {
await this.collapsedActionBtn.first().click();
await expect(this.manualRuleRunActionBtn).toBeVisible();
await this.manualRuleRunActionBtn.click();
await this.modalConfirmationBtn.click();
}
}

View file

@ -0,0 +1,59 @@
/*
* 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 { defineConfig, devices } from '@playwright/test';
import path from 'path';
import { ReadableStream as PolyfillReadableStream } from 'web-streams-polyfill';
import dotenv from 'dotenv';
(globalThis as any).ReadableStream = PolyfillReadableStream;
dotenv.config({ path: path.resolve(__dirname, './.env') });
export default defineConfig({
timeout: 60000,
expect: { timeout: 60000 },
testDir: './tests/',
testMatch: process.env.FILE_PATH || '**/*.spec.ts',
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: 0,
workers: 1,
reporter: [['list', { printSteps: true }]],
use: {
trace: 'on-first-retry',
ignoreHTTPSErrors: true,
baseURL: process.env.KIBANA_URL,
bypassCSP: true,
actionTimeout: 60000,
navigationTimeout: 60000,
screenshot: 'only-on-failure',
launchOptions: {
args: ['--disable-web-security'],
},
},
projects: [
{
name: 'login_ess',
testMatch: '**/setup/login_ess.ts',
},
{
name: 'login_serverless',
testMatch: '**/setup/login_serverless.ts',
},
{
name: 'ess',
use: { ...devices['Desktop Chrome'] },
dependencies: ['login_ess'],
},
{
name: 'serverless',
use: { ...devices['Desktop Chrome'] },
dependencies: ['login_serverless'],
},
],
});

View file

@ -0,0 +1,45 @@
/*
* 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 { expect, test } from '@playwright/test';
import { PageFactory } from '../page_objects/page_factory';
import { EntityAnalyticsPage } from '../page_objects/entity_analytics_po';
import { EntityAnalyticsManagementPage } from '../page_objects/entity_analytics_management_po';
import { createEsArchiver } from '../fixtures/es_archiver';
let entityAnalyticsPage: EntityAnalyticsPage;
let entityAnalyticsManagementPage: EntityAnalyticsManagementPage;
test.beforeAll(async () => {
const esArchiver = await createEsArchiver();
await esArchiver.loadIfNeeded('auditbeat_single');
});
test.describe('Enable risk scores from dashboard', { tag: ['@serverless', '@ess'] }, () => {
test.use({ storageState: '.auth/user.json' });
test.beforeEach(async ({ page }) => {
entityAnalyticsPage = await PageFactory.createEntityAnalyticsPage(page);
await entityAnalyticsPage.navigates();
});
test('host risk enable button should redirect to entity management page', async () => {
await entityAnalyticsPage.waitForEnableHostRiskScoreToBePresent();
entityAnalyticsManagementPage = await entityAnalyticsPage.enableHostRisk();
await expect(entityAnalyticsManagementPage.entityAnalyticsManagementPageTitle).toHaveText(
'Entity Risk Score'
);
});
test('user risk enable button should redirect to entity management page', async () => {
await entityAnalyticsPage.waitForEnableUserRiskScoreToBePresent();
entityAnalyticsManagementPage = await entityAnalyticsPage.enableUserRisk();
await expect(entityAnalyticsManagementPage.entityAnalyticsManagementPageTitle).toHaveText(
'Entity Risk Score'
);
});
});

View file

@ -0,0 +1,58 @@
/*
* 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 { expect, test } from '@playwright/test';
import { PageFactory } from '../page_objects/page_factory';
import { RuleDetailsPage } from '../page_objects/rule_details_page_po';
import { createRule, deleteAllRules } from '../api_utils/rules';
import { deleteAllSecurityDocuments } from '../api_utils/documents';
import { RuleManagementPage } from '../page_objects/rule_management_po';
import { createEsArchiver } from '../fixtures/es_archiver';
let ruleDetailsPage: RuleDetailsPage;
let ruleManagementPage: RuleManagementPage;
test.beforeAll(async () => {
const esArchiver = await createEsArchiver();
await esArchiver.loadIfNeeded('auditbeat_single');
});
test.describe('Manual rule run', { tag: ['@ess', '@serverless'] }, () => {
test.use({ storageState: '.auth/user.json' });
test.beforeEach(async ({ request }) => {
await deleteAllRules(request);
await deleteAllSecurityDocuments(request);
});
test('schedule from rule details page', async ({ request, page }) => {
const { id: ruleId } = await createRule(request);
ruleDetailsPage = await PageFactory.createRuleDetailsPage(page);
await ruleDetailsPage.navigateTo(ruleId);
await ruleDetailsPage.manualRuleRun();
await expect(ruleDetailsPage.toaster).toHaveText(
'Successfully scheduled manual run for 1 rule'
);
});
test('schedule from rules management table', async ({ request, page }) => {
await createRule(request);
ruleManagementPage = await PageFactory.createRuleManagementPage(page);
await ruleManagementPage.navigate();
await ruleManagementPage.disableAutoRefresh();
await ruleManagementPage.manuallyRunFirstRule();
await expect(ruleManagementPage.toaster).toHaveText(
'Successfully scheduled manual run for 1 rule'
);
});
});

View file

@ -0,0 +1,30 @@
/*
* 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 { test } from '@playwright/test';
import { getCommonHeaders } from '../../api_utils/headers';
export const authFile = '.auth/user.json';
test('login', { tag: '@ess' }, async ({ request }) => {
const headers = await getCommonHeaders();
await request.post(`${process.env.KIBANA_URL}/internal/security/login`, {
headers,
data: {
providerType: 'basic',
providerName: 'basic',
currentURL: '/',
params: {
username: process.env.ELASTICSEARCH_USERNAME,
password: process.env.ELASTICSEARCH_PASSWORD,
},
},
});
await request.storageState({ path: authFile });
});

View file

@ -0,0 +1,39 @@
/*
* 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 fs from 'fs';
import path from 'path';
import { test } from '../../fixtures/saml';
export const authFile = '.auth/user.json';
test('serverless', { tag: '@serverless' }, async ({ samlSessionManager }) => {
const cookie = await samlSessionManager.getInteractiveUserSessionCookieWithRoleScope(
'platform_engineer'
);
const parsedUrl = new URL(process.env.KIBANA_URL!);
const domain = parsedUrl.hostname;
const maxAge = 100 * 365 * 24 * 60 * 60;
const authData = {
cookies: [
{
name: 'sid',
value: cookie,
expires: maxAge,
secure: false,
sameSite: 'Lax',
domain,
path: '/',
httpOnly: true,
},
],
};
fs.mkdirSync(path.dirname(authFile), { recursive: true });
fs.writeFileSync(authFile, JSON.stringify(authData, null, 2));
});

View file

@ -0,0 +1,17 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"types": ["node"],
"outDir": "target/types"
},
"include": ["index.d.ts"],
"kbn_references": [
{
"path": "../../plugins/security_solution/tsconfig.json",
"force": true
},
],
"exclude": [
"target/**/*"
]
}

View file

@ -6,7 +6,6 @@
*/
import path from 'path';
// @ts-expect-error we have to check types with "allowJs: false" for now, causing this import to fail
import { REPO_ROOT } from '@kbn/repo-info';
import { FtrConfigProviderContext } from '@kbn/test';

View file

@ -8182,6 +8182,13 @@
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@playwright/test@=1.46.0":
version "1.46.0"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.46.0.tgz#ccea6d22c40ee7fa567e4192fafbdf2a907e2714"
integrity sha512-/QYft5VArOrGRP5pgkrfKksqsKA6CEFyGQ/gjNe6q0y4tZ1aaPfq4gIjudr1s3D+pXyrPRdsy4opKDrjBabE5w==
dependencies:
playwright "1.46.0"
"@pmmmwh/react-refresh-webpack-plugin@^0.5.3":
version "0.5.7"
resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.7.tgz#58f8217ba70069cc6a73f5d7e05e85b458c150e2"
@ -16382,7 +16389,7 @@ dotenv-expand@^5.1.0:
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0"
integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==
dotenv@^16.0.2, dotenv@^16.0.3:
dotenv@^16.0.2, dotenv@^16.0.3, dotenv@^16.4.5:
version "16.4.5"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"
integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==
@ -25349,6 +25356,13 @@ playwright-chromium@=1.38.1:
dependencies:
playwright-core "1.38.1"
playwright-chromium@=1.46.0:
version "1.46.0"
resolved "https://registry.yarnpkg.com/playwright-chromium/-/playwright-chromium-1.46.0.tgz#f24228fec92b380ccc8f5f365b897e9d88b612f6"
integrity sha512-UTHYZsr49XFYRQkpCfaHxL63vfu6uThxR1DrNwnU6qik/OworFcugTOJMWFMoop3QP+ThU8laAMumauLdLZXCQ==
dependencies:
playwright-core "1.46.0"
playwright-core@1.38.0:
version "1.38.0"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.38.0.tgz#cb8e135da1c0b1918b070642372040ed9aa7009a"
@ -25359,6 +25373,20 @@ playwright-core@1.38.1, playwright-core@=1.38.1:
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.38.1.tgz#75a3c470aa9576b7d7c4e274de3d79977448ba08"
integrity sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==
playwright-core@1.46.0:
version "1.46.0"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.46.0.tgz#2336ac453a943abf0dc95a76c117f9d3ebd390eb"
integrity sha512-9Y/d5UIwuJk8t3+lhmMSAJyNP1BUC/DqP3cQJDQQL/oWqAiuPTLgy7Q5dzglmTLwcBRdetzgNM/gni7ckfTr6A==
playwright@1.46.0, playwright@=1.46.0:
version "1.46.0"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.46.0.tgz#c7ff490deae41fc1e814bf2cb62109dd9351164d"
integrity sha512-XYJ5WvfefWONh1uPAUAi0H2xXV5S3vrtcnXe6uAOgdGi3aSpqOSXX08IAjXW34xitfuOJsvXU5anXZxPSEQiJw==
dependencies:
playwright-core "1.46.0"
optionalDependencies:
fsevents "2.3.2"
playwright@=1.38.0:
version "1.38.0"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.38.0.tgz#0ee19d38512b7b1f961c0eb44008a6fed373d206"