[ska] create platform shared package for Cypress test helpers (#224361)

## Summary

Part of https://github.com/elastic/kibana-team/issues/1503

This PR adds `kbn/cypress-test-helper` as platform shared package to
replace invalid imports from private `security-solution` plugin in
platform shared plugin `osquery`.

The reason is that we are **currently blocked with x-pack relocation due
to circular dependency**, e.g. in
https://github.com/elastic/kibana/pull/223897

```

info starting [tsc] > node_modules/typescript/bin/tsc -b tsconfig.refs.json --pretty
--
  | 2025-06-13 13:17:30 UTC | proc [tsc] error TS6202: Project references may not form a circular graph. Cycle detected: /opt/buildkite-agent/builds/bk-agent-prod-gcp-1749820368903967112/elastic/kibana-pull-request/kibana/tsconfig.refs.json
  | 2025-06-13 13:17:30 UTC | proc [tsc] /opt/buildkite-agent/builds/bk-agent-prod-gcp-1749820368903967112/elastic/kibana-pull-request/kibana/x-pack/platform/plugins/shared/osquery/cypress/tsconfig.type_check.json
  | 2025-06-13 13:17:30 UTC | proc [tsc] /opt/buildkite-agent/builds/bk-agent-prod-gcp-1749820368903967112/elastic/kibana-pull-request/kibana/x-pack/test_serverless/tsconfig.type_check.json
  | 2025-06-13 13:17:30 UTC | proc [tsc] /opt/buildkite-agent/builds/bk-agent-prod-gcp-1749820368903967112/elastic/kibana-pull-request/kibana/x-pack/solutions/security/test/tsconfig.type_check.json
  | 2025-06-13 13:17:30 UTC | proc [tsc] /opt/buildkite-agent/builds/bk-agent-prod-gcp-1749820368903967112/elastic/kibana-pull-request/kibana/x-pack/test/security_solution_endpoint/tsconfig.type_check.json
```

**Important:**
This PR focuses only on replacing test helpers imports from
`@kbn/security-solution-plugin` and `@kbn/test-suites-xpack` in
`osquery` plugin, no code cleanup and updates in other plugins / test
packages.
We expect code owners to update other imports / refactor package to
avoid code duplication

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Dzmitry Lemechko 2025-06-24 11:51:12 +03:00 committed by GitHub
parent 1c359dc1d8
commit 539007f65a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 1728 additions and 11 deletions

View file

@ -2268,8 +2268,6 @@ module.exports = {
'src/cli_setup/**', // is importing "@kbn/interactive-setup-plugin" (platform/private)
'src/dev/build/tasks/install_chromium.ts', // is importing "@kbn/screenshotting-plugin" (platform/private)
// FIXME tomsonpl @kbn/osquery-plugin depends on @kbn/security-solution-plugin (security/private) (cypress code => cypress code)
'x-pack/platform/plugins/shared/osquery/**',
// FIXME PhilippeOberti @kbn/timelines-plugin depends on security-solution-plugin (security/private) (timelines is going to disappear)
'x-pack/platform/plugins/shared/timelines/**',
@ -2300,6 +2298,23 @@ module.exports = {
'no-console': 'off',
},
},
{
files: ['x-pack/**/cypress/**/*.ts'],
rules: {
'no-restricted-imports': [
'error',
{
paths: [
{
name: '@kbn/cypress-test-helper',
message:
"Import from a sub-path (e.g. '@kbn/cypress-test-helper/src/utils'). Cypress uses Webpack, which requires direct file imports to avoid parse errors.",
},
],
},
],
},
},
],
};

1
.github/CODEOWNERS vendored
View file

@ -441,6 +441,7 @@ src/platform/packages/shared/kbn-crypto-browser @elastic/kibana-core
src/platform/packages/shared/kbn-css-utils @elastic/appex-sharedux
src/platform/packages/shared/kbn-custom-icons @elastic/obs-ux-logs-team
src/platform/packages/shared/kbn-cypress-config @elastic/kibana-operations
src/platform/packages/shared/kbn-cypress-test-helper @elastic/security-solution
src/platform/packages/shared/kbn-data-grid-in-table-search @elastic/kibana-data-discovery
src/platform/packages/shared/kbn-data-service @elastic/kibana-visualizations @elastic/kibana-data-discovery
src/platform/packages/shared/kbn-data-view-utils @elastic/kibana-data-discovery

View file

@ -1505,6 +1505,7 @@
"@kbn/core-ui-settings-server-mocks": "link:src/core/packages/ui-settings/server-mocks",
"@kbn/core-usage-data-server-mocks": "link:src/core/packages/usage-data/server-mocks",
"@kbn/cypress-config": "link:src/platform/packages/shared/kbn-cypress-config",
"@kbn/cypress-test-helper": "link:src/platform/packages/shared/kbn-cypress-test-helper",
"@kbn/dependency-ownership": "link:packages/kbn-dependency-ownership",
"@kbn/dependency-usage": "link:packages/kbn-dependency-usage",
"@kbn/dev-cli-errors": "link:src/platform/packages/shared/kbn-dev-cli-errors",

View file

@ -0,0 +1,3 @@
# @kbn/cypress-test-helper
Empty package generated by @kbn/generate

View file

@ -0,0 +1,36 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
// / <reference types="cypress" />
declare namespace Cypress {
interface Chainable<Subject = any> {
/**
* Continuously call provided callback function until it either return `true`
* or fail if `timeout` is reached.
* @param fn
* @param options
* @param message
*/
waitUntil(
fn: (subject?: any) => boolean | Promise<boolean> | Chainable<boolean>,
options?: Partial<{
interval: number;
timeout: number;
}>,
message?: string
): Chainable<Subject>;
/**
* Waits for no network activity for a given URL.
* @param url Partial URL to match requests
* @param timeout Optional timeout in ms (default: 500)
*/
waitForNetworkIdle(url: string, timeout?: number): Chainable<Subject>;
}
}

View file

@ -0,0 +1,10 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export * from './src';

View file

@ -0,0 +1,14 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
module.exports = {
preset: '@kbn/test/jest_node',
rootDir: '../../../../..',
roots: ['<rootDir>/src/platform/packages/shared/kbn-cypress-test-helper'],
};

View file

@ -0,0 +1,8 @@
{
"type": "test-helper",
"id": "@kbn/cypress-test-helper",
"owner": "@elastic/security-solution",
"group": "platform",
"visibility": "shared",
"devOnly": true
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/cypress-test-helper",
"private": true,
"version": "1.0.0",
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
}

View file

@ -0,0 +1,29 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export const getApiAuth = () => ({
user: Cypress.env('KIBANA_USERNAME') ?? Cypress.env('ELASTICSEARCH_USERNAME'),
pass: Cypress.env('KIBANA_PASSWORD') ?? Cypress.env('ELASTICSEARCH_PASSWORD'),
});
export const COMMON_API_HEADERS = Object.freeze({
'kbn-xsrf': 'cypress',
'x-elastic-internal-origin': 'security-solution',
'elastic-api-version': '2023-10-31',
});
export const request = <T = unknown>({
headers,
...options
}: Partial<Cypress.RequestOptions>): Cypress.Chainable<Cypress.Response<T>> =>
cy.request<T>({
auth: getApiAuth(),
headers: { ...COMMON_API_HEADERS, ...headers },
...options,
});

View file

@ -0,0 +1,274 @@
{
"reader": {
"name": "reader",
"elasticsearch": {
"cluster": [],
"indices": [
{
"names": [
".siem-signals-*",
".alerts-security*",
".lists*",
".items*",
"metrics-endpoint.metadata_current_*",
".fleet-agents*",
".fleet-actions*"
],
"privileges": ["read"]
},
{
"names": ["*"],
"privileges": ["read", "maintenance", "view_index_metadata"]
}
],
"run_as": []
},
"kibana": [
{
"feature": {
"ml": ["read"],
"siemV2": ["read", "read_alerts"],
"securitySolutionAssistant": ["none"],
"securitySolutionAttackDiscovery": ["none"],
"securitySolutionCasesV2": ["read"],
"securitySolutionTimeline": ["read"],
"securitySolutionNotes": ["read"],
"actions": ["read"],
"builtInAlerts": ["read"]
},
"spaces": ["*"],
"base": []
}
]
},
"hunter": {
"name": "hunter",
"elasticsearch": {
"cluster": [],
"indices": [
{
"names": [
"apm-*-transaction*",
"traces-apm*",
"auditbeat-*",
"endgame-*",
"filebeat-*",
"logs-*",
"packetbeat-*",
"winlogbeat-*"
],
"privileges": ["read", "write"]
},
{
"names": [".alerts-security*", ".siem-signals-*"],
"privileges": ["read", "write"]
},
{
"names": [".lists*", ".items*"],
"privileges": ["read", "write"]
},
{
"names": ["metrics-endpoint.metadata_current_*", ".fleet-agents*", ".fleet-actions*"],
"privileges": ["read"]
}
],
"run_as": []
},
"kibana": [
{
"feature": {
"ml": ["read"],
"siemV2": ["all", "read_alerts", "crud_alerts"],
"securitySolutionAssistant": ["all"],
"securitySolutionAttackDiscovery": ["all"],
"securitySolutionCasesV2": ["all"],
"securitySolutionTimeline": ["read"],
"securitySolutionNotes": ["read"],
"actions": ["read"],
"builtInAlerts": ["all"]
},
"spaces": ["*"],
"base": []
}
]
},
"hunter_no_actions": {
"name": "hunter_no_actions",
"elasticsearch": {
"cluster": [],
"indices": [
{
"names": [
"apm-*-transaction*",
"traces-apm*",
"auditbeat-*",
"endgame-*",
"filebeat-*",
"logs-*",
"packetbeat-*",
"winlogbeat-*"
],
"privileges": ["read", "write"]
},
{
"names": [".alerts-security*", ".siem-signals-*"],
"privileges": ["read", "write"]
},
{
"names": [".lists*", ".items*"],
"privileges": ["read", "write"]
},
{
"names": ["metrics-endpoint.metadata_current_*", ".fleet-agents*", ".fleet-actions*"],
"privileges": ["read"]
}
],
"run_as": []
},
"kibana": [
{
"feature": {
"ml": ["read"],
"siemV2": ["all", "read_alerts", "crud_alerts"],
"securitySolutionAssistant": ["all"],
"securitySolutionAttackDiscovery": ["all"],
"securitySolutionCasesV2": ["all"],
"securitySolutionTimeline": ["all"],
"securitySolutionNotes": ["all"],
"builtInAlerts": ["all"]
},
"spaces": ["*"],
"base": []
}
]
},
"no_risk_engine_privileges": {
"name": "no_risk_engine_privileges",
"elasticsearch": {
"cluster": [],
"indices": [],
"run_as": []
},
"kibana": [
{
"feature": {
"siemV2": ["read"]
},
"spaces": ["*"],
"base": []
}
]
},
"timeline_none": {
"name": "timeline_none",
"elasticsearch": {
"cluster": [],
"indices": [
{
"names": [
"apm-*-transaction*",
"traces-apm*",
"auditbeat-*",
"endgame-*",
"filebeat-*",
"logs-*",
"packetbeat-*",
"winlogbeat-*",
".lists*",
".items*",
".asset-criticality.asset-criticality-*"
],
"privileges": ["read", "write"]
},
{
"names": [
".alerts-security*",
".preview.alerts-security*",
".internal.preview.alerts-security*",
".adhoc.alerts-security*",
".internal.adhoc.alerts-security*",
".siem-signals-*"
],
"privileges": ["read", "write", "manage"]
},
{
"names": ["metrics-endpoint.metadata_current_*", ".fleet-agents*", ".fleet-actions*"],
"privileges": ["read"]
}
],
"run_as": []
},
"kibana": [
{
"feature": {
"ml": ["read"],
"siemV2": ["all", "read_alerts", "crud_alerts"],
"securitySolutionAssistant": ["all"],
"securitySolutionAttackDiscovery": ["all"],
"securitySolutionCasesV2": ["all"],
"securitySolutionNotes": ["all"],
"actions": ["all"],
"builtInAlerts": ["all"]
},
"spaces": ["*"],
"base": []
}
]
},
"notes_none": {
"name": "notes_none",
"elasticsearch": {
"cluster": [],
"indices": [
{
"names": [
"apm-*-transaction*",
"traces-apm*",
"auditbeat-*",
"endgame-*",
"filebeat-*",
"logs-*",
"packetbeat-*",
"winlogbeat-*",
".lists*",
".items*",
".asset-criticality.asset-criticality-*"
],
"privileges": ["read", "write"]
},
{
"names": [
".alerts-security*",
".preview.alerts-security*",
".internal.preview.alerts-security*",
".adhoc.alerts-security*",
".internal.adhoc.alerts-security*",
".siem-signals-*"
],
"privileges": ["read", "write", "manage"]
},
{
"names": ["metrics-endpoint.metadata_current_*", ".fleet-agents*", ".fleet-actions*"],
"privileges": ["read"]
}
],
"run_as": []
},
"kibana": [
{
"feature": {
"ml": ["read"],
"siemV2": ["all", "read_alerts", "crud_alerts"],
"securitySolutionAssistant": ["all"],
"securitySolutionAttackDiscovery": ["all"],
"securitySolutionCasesV2": ["all"],
"securitySolutionTimeline": ["all"],
"actions": ["all"],
"builtInAlerts": ["all"]
},
"spaces": ["*"],
"base": []
}
]
}
}

View file

@ -0,0 +1,54 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import serverlessRoleDefinitions from '@kbn/es/src/serverless_resources/security_roles.json';
import essRoleDefinitions from './ess_roles.json';
type ServerlessSecurityRoleName = keyof typeof serverlessRoleDefinitions;
type EssSecurityRoleName = keyof typeof essRoleDefinitions;
export const KNOWN_SERVERLESS_ROLE_DEFINITIONS = serverlessRoleDefinitions;
export const KNOWN_ESS_ROLE_DEFINITIONS = essRoleDefinitions;
export type SecurityRoleName = ServerlessSecurityRoleName | EssSecurityRoleName;
export enum ROLES {
// Serverless roles
t1_analyst = 't1_analyst',
t2_analyst = 't2_analyst',
t3_analyst = 't3_analyst',
rule_author = 'rule_author',
soc_manager = 'soc_manager',
detections_admin = 'detections_admin',
platform_engineer = 'platform_engineer',
// ESS roles
reader = 'reader',
hunter = 'hunter',
hunter_no_actions = 'hunter_no_actions',
no_risk_engine_privileges = 'no_risk_engine_privileges',
timeline_none = 'timeline_none',
notes_none = 'notes_none',
}
/**
* Provides a map of the commonly used date ranges found under the Quick Menu popover of the
* super date picker component.
*/
export const DATE_RANGE_OPTION_TO_TEST_SUBJ_MAP = Object.freeze({
Today: 'superDatePickerCommonlyUsed_Today',
'This week': 'superDatePickerCommonlyUsed_This_week',
'Last 15 minutes': 'superDatePickerCommonlyUsed_Last_15 minutes',
'Last 30 minutes': 'superDatePickerCommonlyUsed_Last_30 minutes',
'Last 1 hour': 'superDatePickerCommonlyUsed_Last_1 hour',
'Last 24 hours': 'superDatePickerCommonlyUsed_Last_24 hours',
'Last 7 days': 'superDatePickerCommonlyUsed_Last_7 days',
'Last 30 days': 'superDatePickerCommonlyUsed_Last_30 days',
'Last 90 days': 'superDatePickerCommonlyUsed_Last_90 days',
'Last 1 year': 'superDatePickerCommonlyUsed_Last_1 year',
});

View file

@ -0,0 +1,22 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export { login } from './login';
export { samlAuthentication, resolveCloudUsersFilePath, ES_LOADED_USERS } from './saml_auth';
export type {
EndpointSecurityRoleNames,
EndpointSecurityRoleDefinitions,
KibanaKnownUserAccounts,
SecurityTestUser,
} from './roles_and_users';
export {
SECURITY_SERVERLESS_ROLE_NAMES,
ENDPOINT_SECURITY_ROLE_NAMES,
KIBANA_KNOWN_DEFAULT_ACCOUNTS,
} from './roles_and_users';

View file

@ -0,0 +1,162 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { LoginState } from '@kbn/security-plugin/common/login_state';
import type { Role } from '@kbn/security-plugin/common';
import { ENDPOINT_SECURITY_ROLE_NAMES, KIBANA_KNOWN_DEFAULT_ACCOUNTS } from './roles_and_users';
import type { SecurityTestUser } from './roles_and_users';
import { COMMON_API_HEADERS, request } from '../api';
export const ROLE = Object.freeze<Record<SecurityTestUser, SecurityTestUser>>({
...ENDPOINT_SECURITY_ROLE_NAMES,
...KIBANA_KNOWN_DEFAULT_ACCOUNTS,
});
interface CyLoginTask {
(user?: SecurityTestUser): ReturnType<typeof sendApiLoginRequest>;
/**
* Login using any username/password
* @param username
* @param password
*/
with(username: string, password: string): ReturnType<typeof sendApiLoginRequest>;
/**
* Creates the provided role in kibana/ES along with a respective user (same name as role)
* and then login with this new user
* @param role
*/
withCustomRole(role: Role): ReturnType<typeof sendApiLoginRequest>;
}
/**
* Login to Kibana using API (not login page).
* By default, user will be logged in using `KIBANA_USERNAME` and `KIBANA_PASSWORD` retrieved from
* the cypress `env`
*
* @param user
*/
export const login: CyLoginTask = (
user: SecurityTestUser = ROLE.endpoint_operations_analyst
): ReturnType<typeof sendApiLoginRequest> => {
let username = Cypress.env('KIBANA_USERNAME');
let password = Cypress.env('KIBANA_PASSWORD');
const isServerless = Cypress.env('IS_SERVERLESS');
const isCloudServerless = Cypress.env('CLOUD_SERVERLESS');
if (isServerless && isCloudServerless) {
// MKI QA Cloud Serverless
return cy
.task('getSessionCookie', user)
.then((result) => {
const {
username: u,
password: p,
cookie,
} = result as {
username: string;
password: string;
cookie: string;
};
username = u;
password = p;
// Set cookie asynchronously
return cy.setCookie('sid', cookie, {
// "hostOnly: true" sets the cookie without a domain.
// This makes cookie available only for the current host (not subdomains).
// It's needed to match the Serverless backend behavior where cookies are set without a domain.
// More info: https://github.com/elastic/kibana/issues/221741
hostOnly: true,
});
})
.then(() => {
// Visit URL after setting cookie
return cy.visit('/');
})
.then(() => {
cy.getCookies().then((cookies) => {
// Ensure that there's only a single session cookie named 'sid'.
const sessionCookies = cookies.filter((cookie) => cookie.name === 'sid');
expect(sessionCookies).to.have.length(1);
});
})
.then(() => {
// Return username and password
return { username, password };
});
} else if (user) {
return cy.task('loadUserAndRole', { name: user }).then((loadedUser) => {
const { username: u, password: p } = loadedUser as {
username: string;
password: string;
};
username = u;
password = p;
return sendApiLoginRequest(username, password);
});
} else {
return sendApiLoginRequest(username, password);
}
};
login.with = (username: string, password: string): ReturnType<typeof sendApiLoginRequest> => {
return sendApiLoginRequest(username, password);
};
login.withCustomRole = (role: Role): ReturnType<typeof sendApiLoginRequest> => {
return cy.task('createUserAndRole', { role }).then((result) => {
const { username, password } = result as {
username: string;
password: string;
};
return sendApiLoginRequest(username, password);
});
};
/**
* Send login via API
* @param username
* @param password
*
* @private
*/
const sendApiLoginRequest = (
username: string,
password: string
): Cypress.Chainable<{ username: string; password: string }> => {
const baseUrl = Cypress.config().baseUrl;
const loginUrl = `${baseUrl}/internal/security/login`;
const headers = { ...COMMON_API_HEADERS };
cy.log(`Authenticating [${username}] via ${loginUrl}`);
return request<LoginState>({ headers, url: `${baseUrl}/internal/security/login_state` })
.then((loginState) => {
const basicProvider = loginState.body.selector.providers.find(
(provider) => provider.type === 'basic'
);
return request({
url: loginUrl,
method: 'POST',
headers,
body: {
providerType: basicProvider?.type,
providerName: basicProvider?.name,
currentURL: '/',
params: { username, password },
},
});
})
.then(() => ({ username, password }));
};

View file

@ -0,0 +1,65 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { Role } from '@kbn/security-plugin/common';
export type EndpointSecurityRoleNames = keyof typeof ENDPOINT_SECURITY_ROLE_NAMES;
export type EndpointSecurityRoleDefinitions = Record<EndpointSecurityRoleNames, Role>;
/**
* Security Solution set of roles that are loaded and used in serverless deployments.
* The source of these role definitions is under `project-controller` at:
*
* @see https://github.com/elastic/project-controller/blob/main/internal/project/security/config/roles.yml
*
* The role definition spreadsheet can be found here:
*
* @see https://docs.google.com/spreadsheets/d/16aGow187AunLCBFZLlbVyS81iQNuMpNxd96LOerWj4c/edit#gid=1936689222
*/
export const SECURITY_SERVERLESS_ROLE_NAMES = Object.freeze({
t1_analyst: 't1_analyst',
t2_analyst: 't2_analyst',
t3_analyst: 't3_analyst',
threat_intelligence_analyst: 'threat_intelligence_analyst',
rule_author: 'rule_author',
soc_manager: 'soc_manager',
detections_admin: 'detections_admin',
platform_engineer: 'platform_engineer',
endpoint_operations_analyst: 'endpoint_operations_analyst',
endpoint_policy_manager: 'endpoint_policy_manager',
});
export const ENDPOINT_SECURITY_ROLE_NAMES = Object.freeze({
// --------------------------------------
// Set of roles used in serverless
...SECURITY_SERVERLESS_ROLE_NAMES,
// --------------------------------------
// Other roles used for testing
hunter: 'hunter',
endpoint_response_actions_access: 'endpoint_response_actions_access',
endpoint_response_actions_no_access: 'endpoint_response_actions_no_access',
endpoint_security_policy_management_read: 'endpoint_security_policy_management_read',
artifact_read_privileges: 'artifact_read_privileges',
});
export type KibanaKnownUserAccounts = keyof typeof KIBANA_KNOWN_DEFAULT_ACCOUNTS;
export type SecurityTestUser = EndpointSecurityRoleNames | KibanaKnownUserAccounts;
/**
* List of kibana system accounts
*/
export const KIBANA_KNOWN_DEFAULT_ACCOUNTS = {
elastic: 'elastic',
elastic_serverless: 'elastic_serverless',
system_indices_superuser: 'system_indices_superuser',
admin: 'admin',
} as const;

View file

@ -0,0 +1,80 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ToolingLog } from '@kbn/tooling-log';
import { resolve, join } from 'path';
import { readFileSync } from 'fs';
import { REPO_ROOT } from '@kbn/repo-info';
import type { HostOptions } from '@kbn/test';
import { SamlSessionManager } from '@kbn/test';
import type { SecurityRoleName } from './common';
const ES_RESOURCES_DIR = resolve(
REPO_ROOT,
'x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/serverless/es_serverless_resources'
);
export const ES_RESOURCES = Object.freeze({
roles: join(ES_RESOURCES_DIR, 'roles.yml'),
users: join(ES_RESOURCES_DIR, 'users'),
users_roles: join(ES_RESOURCES_DIR, 'users_roles'),
});
export const resolveCloudUsersFilePath = (filename: string) => resolve(REPO_ROOT, '.ftr', filename);
export const ES_LOADED_USERS = readFileSync(ES_RESOURCES.users)
.toString()
.split(/\n/)
.filter((v) => !!v) // Ensure no empty strings
.map((userAndPasswordString) => {
return userAndPasswordString.split(':').at(0);
});
export const samlAuthentication = async (
on: Cypress.PluginEvents,
config: Cypress.PluginConfigOptions
): Promise<void> => {
const log = new ToolingLog({ level: 'verbose', writeTo: process.stdout });
const kbnHost = config.env.KIBANA_URL || config.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: config.env.ELASTICSEARCH_USERNAME,
password: config.env.ELASTICSEARCH_PASSWORD,
};
on('task', {
getSessionCookie: async (
role: string | SecurityRoleName
): Promise<{ cookie: string; username: string; password: string }> => {
// If config.env.PROXY_ORG is set, it means that proxy service is used to create projects. Define the proxy org filename to override the roles.
const rolesFilename = config.env.PROXY_ORG
? `${config.env.PROXY_ORG}.json`
: 'role_users.json';
const sessionManager = new SamlSessionManager({
hostOptions,
log,
isCloud: config.env.CLOUD_SERVERLESS,
cloudUsersFilePath: resolveCloudUsersFilePath(rolesFilename),
});
return sessionManager.getInteractiveUserSessionCookieWithRoleScope(role).then((cookie) => {
return {
cookie,
username: hostOptions.username,
password: hostOptions.password,
};
});
},
});
};

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
/**
* Endpoint base error class that supports an optional second argument for providing additional data
* for the error.
*/
export class EndpointError<MetaType = unknown> extends Error {
constructor(message: string, public readonly meta?: MetaType) {
super(message);
// For debugging - capture name of subclasses
this.name = this.constructor.name;
if (meta instanceof Error) {
this.stack += `\n----- original error -----\n${meta.stack}`;
}
}
}
/**
* Type guard to check if a given Error is an instance of EndpointError
* @param err
*/
export const isEndpointError = (err: Error): err is EndpointError => {
return err instanceof EndpointError;
};

View file

@ -0,0 +1,78 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { AxiosError } from 'axios';
import { EndpointError } from './errors';
export class FormattedAxiosError extends EndpointError {
public readonly request: {
method: string;
url: string;
data: unknown;
};
public readonly response: {
status: number;
statusText: string;
data: any;
};
constructor(axiosError: AxiosError) {
const method = axiosError.config?.method ?? '';
const url = axiosError.config?.url ?? '';
super(
`${axiosError.message}${
axiosError?.response?.data ? `: ${JSON.stringify(axiosError?.response?.data)}` : ''
}${url ? `\n(Request: ${method} ${url})` : ''}`,
axiosError
);
this.request = {
method,
url,
data: axiosError.config?.data ?? '',
};
this.response = {
status: axiosError?.response?.status ?? 0,
statusText: axiosError?.response?.statusText ?? '',
data: axiosError?.response?.data,
};
this.name = this.constructor.name;
}
toJSON() {
return {
message: this.message,
request: this.request,
response: this.response,
};
}
toString() {
return JSON.stringify(this.toJSON(), null, 2);
}
}
/**
* Used with `promise.catch()`, it will format the Axios error to a new error and will re-throw
* @param error
*/
export const catchAxiosErrorFormatAndThrow = (error: Error): never => {
if (error instanceof AxiosError) {
throw new FormattedAxiosError(error);
}
if (!(error instanceof EndpointError)) {
throw new EndpointError(error.message, error);
}
throw error;
};

View file

@ -0,0 +1,11 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export * from './errors';
export * from './format_axios_error';

View file

@ -0,0 +1,22 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export * from './auth';
export * from './error';
export { enableFleetSpaceAwareness } from './services/fleet_services';
export { isLocalhost } from './services/is_localhost';
export { fetchKibanaStatus, isServerlessKibanaFlavor } from './services/kibana_status';
export { getLocalhostRealIp } from './services/network_services';
export { createSecuritySuperuser } from './services/security_user_services';
export type { CreatedSecuritySuperuser } from './services/security_user_services';
export { createRuntimeServices } from './services/stack_services';
export { waitForAlertsToPopulate } from './services/alerting_services';
export * from './api';
export { createToolingLogger } from './logger';
export * from './utils';

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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { ToolingLogTextWriterConfig } from '@kbn/tooling-log';
import { ToolingLog } from '@kbn/tooling-log';
import type { Flags } from '@kbn/dev-cli-runner';
interface CreateLoggerInterface {
(level?: Partial<ToolingLogTextWriterConfig>['level']): ToolingLog;
/**
* The default log level if one is not provided to the `createToolingLogger()` utility.
* Can be used to globally set the log level to calls made to this utility with no `level` set
* on input.
*/
defaultLogLevel: ToolingLogTextWriterConfig['level'];
/**
* Set the default logging level based on the flag arguments provide to a CLI script that runs
* via `@kbn/dev-cli-runner`
* @param flags
*/
setDefaultLogLevelFromCliFlags: (flags: Flags) => void;
}
/**
* Creates an instance of `ToolingLog` that outputs to `stdout`.
* The default log `level` for all instances can be set by setting the function's `defaultLogLevel`
* property. Default logging level can also be set from CLI scripts that use the `@kbn/dev-cli-runner`
* by calling the `setDefaultLogLevelFromCliFlags(flags)` and passing in the `flags` property.
*
* @param level
*
* @example
* // Set default log level - example: from cypress for CI jobs
* createLogger.defaultLogLevel = 'verbose'
*/
export const createToolingLogger: CreateLoggerInterface = (level): ToolingLog => {
return new ToolingLog({
level: level || createToolingLogger.defaultLogLevel,
writeTo: process.stdout,
});
};
createToolingLogger.defaultLogLevel = 'info';
createToolingLogger.setDefaultLogLevelFromCliFlags = (flags) => {
createToolingLogger.defaultLogLevel = flags.verbose
? 'verbose'
: flags.debug
? 'debug'
: flags.silent
? 'silent'
: flags.quiet
? 'error'
: 'info';
};

View file

@ -0,0 +1,75 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import 'cypress-network-idle';
const GLOBAL_KQL_WRAPPER = '[data-test-subj="filters-global-container"]';
const REFRESH_BUTTON = `${GLOBAL_KQL_WRAPPER} [data-test-subj="querySubmitButton"]`;
const DATAGRID_CHANGES_IN_PROGRESS = '[data-test-subj="body-data-grid"] .euiProgress';
const EVENT_CONTAINER_TABLE_LOADING = '[data-test-subj="internalAlertsPageLoading"]';
const LOADING_INDICATOR = '[data-test-subj="globalLoadingIndicator"]';
const EMPTY_ALERT_TABLE = '[data-test-subj="alertsTableEmptyState"]';
const ALERTS_TABLE_COUNT = `[data-test-subj="toolbar-alerts-count"]`;
const DETECTION_PAGE_FILTER_GROUP_WRAPPER = '.filter-group__wrapper';
const DETECTION_PAGE_FILTERS_LOADING = '.securityPageWrapper .controlFrame--controlLoading';
const DETECTION_PAGE_FILTER_GROUP_LOADING = '[data-test-subj="filter-group__loading"]';
const OPTION_LISTS_LOADING = '.optionsList--filterBtnWrapper .euiLoadingSpinner';
const ALERTS_URL = '/app/security/alerts';
const refreshPage = () => {
cy.get(REFRESH_BUTTON).click({ force: true });
cy.get(REFRESH_BUTTON).should('not.have.attr', 'aria-label', 'Needs updating');
};
const waitForPageFilters = () => {
cy.log('Waiting for Page Filters');
cy.url().then((urlString) => {
const url = new URL(urlString);
if (url.pathname.endsWith(ALERTS_URL)) {
// since these are only valid on the alert page
cy.get(DETECTION_PAGE_FILTER_GROUP_WRAPPER).should('exist');
cy.get(DETECTION_PAGE_FILTER_GROUP_LOADING).should('not.exist');
cy.get(DETECTION_PAGE_FILTERS_LOADING).should('not.exist');
cy.get(OPTION_LISTS_LOADING).should('have.lengthOf', 0);
} else {
cy.log('Skipping Page Filters Wait');
}
});
};
const waitForAlerts = () => {
waitForPageFilters();
cy.get(REFRESH_BUTTON).should('not.have.attr', 'aria-label', 'Needs updating');
cy.get(DATAGRID_CHANGES_IN_PROGRESS).should('not.be.true');
cy.get(EVENT_CONTAINER_TABLE_LOADING).should('not.exist');
cy.get(LOADING_INDICATOR).should('not.exist');
cy.waitForNetworkIdle('/internal/search/privateRuleRegistryAlertsSearchStrategy', 500);
};
export const waitForAlertsToPopulate = (alertCountThreshold = 1) => {
cy.waitUntil(
() => {
cy.log('Waiting for alerts to appear');
refreshPage();
cy.get([EMPTY_ALERT_TABLE, ALERTS_TABLE_COUNT].join(', '));
return cy.root().then(($el) => {
const emptyTableState = $el.find(EMPTY_ALERT_TABLE);
if (emptyTableState.length > 0) {
cy.log('Table is empty', emptyTableState.length);
return false;
}
const countEl = $el.find(ALERTS_TABLE_COUNT);
const alertCount = parseInt(countEl.text(), 10) || 0;
return alertCount >= alertCountThreshold;
});
},
{ interval: 500, timeout: 30000 }
);
waitForAlerts();
};

View file

@ -0,0 +1,26 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { memoize } from 'lodash';
import type { KbnClient } from '@kbn/test';
import { catchAxiosErrorFormatAndThrow } from '../error';
/**
* Calls the Fleet internal API to enable space awareness
* @param kbnClient
*/
export const enableFleetSpaceAwareness = memoize(async (kbnClient: KbnClient): Promise<void> => {
await kbnClient
.request({
path: '/internal/fleet/enable_space_awareness',
headers: { 'Elastic-Api-Version': '1' },
method: 'POST',
})
.catch(catchAxiosErrorFormatAndThrow);
});

View file

@ -0,0 +1,20 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
const POSSIBLE_LOCALHOST_VALUES: readonly string[] = [
'localhost',
'127.0.0.1',
'0.0.0.0',
'::1',
'0000:0000:0000:0000:0000:0000:0000:0000',
];
export const isLocalhost = (hostname: string): boolean => {
return POSSIBLE_LOCALHOST_VALUES.includes(hostname.toLowerCase());
};

View file

@ -0,0 +1,48 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { KbnClient } from '@kbn/test';
import type { Client } from '@elastic/elasticsearch';
import type { StatusResponse } from '@kbn/core-status-common';
import { catchAxiosErrorFormatAndThrow } from '../error';
export const fetchKibanaStatus = async (kbnClient: KbnClient): Promise<StatusResponse> => {
// We DO NOT use `kbnClient.status.get()` here because the `kbnClient` passed on input could be our enhanced
// client (created by `x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/stack_services.ts:267`)
// which could be using an API key (which the core KbnClient does not support)
return kbnClient
.request<StatusResponse>({
method: 'GET',
path: '/api/status',
})
.then(({ data }) => data)
.catch(catchAxiosErrorFormatAndThrow);
};
/**
* Checks to see if Kibana/ES is running in serverless mode
* @param client
*/
export const isServerlessKibanaFlavor = async (client: KbnClient | Client): Promise<boolean> => {
if (client instanceof KbnClient) {
const kbnStatus = await fetchKibanaStatus(client);
// If we don't have status for plugins, then error
// the Status API will always return something (its an open API), but if auth was successful,
// it will also return more data.
if (!kbnStatus?.status?.plugins) {
throw new Error(
`Unable to retrieve Kibana plugins status (likely an auth issue with the username being used for kibana)`
);
}
return kbnStatus.status.plugins?.serverless?.level === 'available';
} else {
return (await client.info()).version.build_flavor === 'serverless';
}
};

View file

@ -0,0 +1,31 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export const getLocalhostRealIp = (): string => {
// Use require dynamically so Cypress don't process it
// eslint-disable-next-line @typescript-eslint/no-var-requires
const os = require('os') as typeof import('os');
const interfaces = os.networkInterfaces();
for (const netInterfaceList of Object.values(interfaces).reverse()) {
if (netInterfaceList) {
const netInterface = netInterfaceList.find(
(networkInterface) =>
networkInterface.family === 'IPv4' &&
!networkInterface.internal &&
networkInterface.address
);
if (netInterface) {
return netInterface.address;
}
}
}
return '0.0.0.0';
};

View file

@ -0,0 +1,78 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { Client } from '@elastic/elasticsearch';
export interface CreatedSecuritySuperuser {
username: string;
password: string;
created: boolean;
}
export const createSecuritySuperuser = async (
esClient: Client,
username?: string,
password: string = 'changeme'
): Promise<CreatedSecuritySuperuser> => {
// Dynamically load userInfo from 'os' if no username is provided
if (!username) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const os = require('os') as typeof import('os');
username = os.userInfo().username;
}
if (!username || !password) {
throw new Error(`username and password require values.`);
}
// Create a role which has full access to restricted indexes
await esClient.transport.request({
method: 'POST',
path: '_security/role/superuser_restricted_indices',
body: {
cluster: ['all'],
indices: [
{
names: ['*'],
privileges: ['all'],
allow_restricted_indices: true,
},
{
names: ['*'],
privileges: ['monitor', 'read', 'view_index_metadata', 'read_cross_cluster'],
allow_restricted_indices: true,
},
],
applications: [
{
application: '*',
privileges: ['*'],
resources: ['*'],
},
],
run_as: ['*'],
},
});
const addedUser = await esClient.transport.request<Promise<{ created: boolean }>>({
method: 'POST',
path: `_security/user/${username}`,
body: {
password,
roles: ['superuser', 'kibana_system', 'superuser_restricted_indices'],
full_name: username,
},
});
return {
created: addedUser.created,
username,
password,
};
};

View file

@ -0,0 +1,383 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { Client, HttpConnection } from '@elastic/elasticsearch';
import type { ToolingLog } from '@kbn/tooling-log';
import type { KbnClientOptions } from '@kbn/test';
import { KbnClient } from '@kbn/test';
import pRetry from 'p-retry';
import type { ReqOptions } from '@kbn/test/src/kbn_client/kbn_client_requester';
import { type AxiosResponse } from 'axios';
import type { ClientOptions } from '@elastic/elasticsearch/lib/client';
import fs from 'fs';
import { CA_CERT_PATH } from '@kbn/dev-utils';
import { omit } from 'lodash';
import { addSpaceIdToPath, DEFAULT_SPACE_ID, getSpaceIdFromPath } from '@kbn/spaces-plugin/common';
import { enableFleetSpaceAwareness } from './fleet_services';
import { fetchKibanaStatus, isServerlessKibanaFlavor } from './kibana_status';
import { createToolingLogger } from '../logger';
import { isLocalhost } from './is_localhost';
import { getLocalhostRealIp } from './network_services';
import { createSecuritySuperuser } from './security_user_services';
const CA_CERTIFICATE: Buffer = fs.readFileSync(CA_CERT_PATH);
export interface RuntimeServices {
kbnClient: KbnClient;
esClient: Client;
log: ToolingLog;
user: Readonly<{
username: string;
password: string;
}>;
apiKey: string;
localhostRealIp: string;
kibana: {
url: string;
hostname: string;
port: string;
isLocalhost: boolean;
};
elastic: {
url: string;
hostname: string;
port: string;
isLocalhost: boolean;
};
fleetServer: {
url: string;
hostname: string;
port: string;
isLocalhost: boolean;
};
}
interface CreateRuntimeServicesOptions {
kibanaUrl: string;
elasticsearchUrl: string;
fleetServerUrl?: string;
username: string;
password: string;
/** The space id in kibana */
spaceId?: string;
/** If defined, both `username` and `password` will be ignored */
apiKey?: string;
/** If undefined, ES username defaults to `username` */
esUsername?: string;
/** If undefined, ES password defaults to `password` */
esPassword?: string;
log?: ToolingLog;
asSuperuser?: boolean;
/** If true, then a certificate will not be used when creating the Kbn/Es clients when url is `https` */
useCertForSsl?: boolean;
}
class KbnClientExtended extends KbnClient {
private readonly apiKey: string | undefined;
constructor(protected readonly options: KbnClientOptions & { apiKey?: string }) {
const { apiKey, url, ...opt } = options;
super({
...opt,
url: apiKey ? buildUrlWithCredentials(url, '', '') : url,
});
this.apiKey = apiKey;
}
async request<T>(options: ReqOptions): Promise<AxiosResponse<T>> {
const headers: ReqOptions['headers'] = {
...(options.headers ?? {}),
};
if (this.apiKey) {
headers.Authorization = `ApiKey ${this.apiKey}`;
this.options.log.verbose(`Adding API key header to request header 'Authorization'`);
}
return super.request({
...options,
headers,
});
}
}
export const createRuntimeServices = async ({
kibanaUrl: _kibanaUrl,
elasticsearchUrl,
fleetServerUrl = 'https://localhost:8220',
username: _username,
password: _password,
spaceId,
apiKey,
esUsername: _esUsername,
esPassword: _esPassword,
log = createToolingLogger(),
asSuperuser = false,
useCertForSsl = false,
}: CreateRuntimeServicesOptions): Promise<RuntimeServices> => {
const kibanaUrl = spaceId ? buildUrlWithSpaceId(_kibanaUrl, spaceId) : _kibanaUrl;
let username = _username;
let password = _password;
let esUsername = _esUsername;
let esPassword = _esPassword;
if (asSuperuser) {
const tmpKbnClient = createKbnClient({
url: kibanaUrl,
username,
password,
useCertForSsl,
log,
spaceId,
});
await waitForKibana(tmpKbnClient);
const isServerlessEs = await isServerlessKibanaFlavor(tmpKbnClient);
if (isServerlessEs) {
log?.warning(
'Creating Security Superuser is not supported in current environment.\nES is running in serverless mode. ' +
'Will use username [system_indices_superuser] instead.'
);
username = 'system_indices_superuser';
password = 'changeme';
esUsername = 'system_indices_superuser';
esPassword = 'changeme';
} else {
const superuserResponse = await createSecuritySuperuser(
createEsClient({
url: elasticsearchUrl,
username: esUsername ?? username,
password: esPassword ?? password,
log,
useCertForSsl,
})
);
({ username, password } = superuserResponse);
if (superuserResponse.created) {
log.info(`Kibana user [${username}] was created with password [${password}]`);
}
}
}
const kbnURL = new URL(kibanaUrl);
const esURL = new URL(elasticsearchUrl);
const fleetURL = new URL(fleetServerUrl);
const kbnClient = createKbnClient({
log,
url: kibanaUrl,
username,
password,
spaceId,
apiKey,
useCertForSsl,
});
if (spaceId && spaceId !== DEFAULT_SPACE_ID) {
log?.info(`Enabling Fleet space awareness`);
await enableFleetSpaceAwareness(kbnClient);
}
return {
kbnClient,
esClient: createEsClient({
log,
url: elasticsearchUrl,
username: esUsername ?? username,
password: esPassword ?? password,
apiKey,
useCertForSsl,
}),
log,
localhostRealIp: getLocalhostRealIp(),
apiKey: apiKey ?? '',
user: {
username,
password,
},
kibana: {
url: kibanaUrl,
hostname: kbnURL.hostname,
port: kbnURL.port,
isLocalhost: isLocalhost(kbnURL.hostname),
},
fleetServer: {
url: fleetServerUrl,
hostname: fleetURL.hostname,
port: fleetURL.port,
isLocalhost: isLocalhost(fleetURL.hostname),
},
elastic: {
url: elasticsearchUrl,
hostname: esURL.hostname,
port: esURL.port,
isLocalhost: isLocalhost(esURL.hostname),
},
};
};
export const buildUrlWithCredentials = (
url: string,
username: string,
password: string
): string => {
const newUrl = new URL(url);
newUrl.username = username;
newUrl.password = password;
return newUrl.href;
};
export const createEsClient = ({
url,
username,
password,
apiKey,
log,
useCertForSsl = false,
}: {
url: string;
username: string;
password: string;
/** If defined, both `username` and `password` will be ignored */
apiKey?: string;
log?: ToolingLog;
useCertForSsl?: boolean;
}): Client => {
const isHttps = new URL(url).protocol.startsWith('https');
const clientOptions: ClientOptions = {
node: buildUrlWithCredentials(url, apiKey ? '' : username, apiKey ? '' : password),
Connection: HttpConnection,
requestTimeout: 30_000,
};
if (isHttps && useCertForSsl) {
clientOptions.tls = {
ca: [CA_CERTIFICATE],
};
}
if (apiKey) {
clientOptions.auth = { apiKey };
}
if (log) {
log.verbose(
`Creating Elasticsearch client options: ${JSON.stringify({
...omit(clientOptions, 'tls'),
...(clientOptions.tls ? { tls: { ca: [typeof clientOptions.tls.ca] } } : {}),
})}`
);
}
return new Client(clientOptions);
};
export const createKbnClient = ({
url: _url,
username,
password,
spaceId,
apiKey,
log = createToolingLogger(),
useCertForSsl = false,
}: {
url: string;
username: string;
password: string;
/** If defined, both `username` and `password` will be ignored */
apiKey?: string;
spaceId?: string;
log?: ToolingLog;
useCertForSsl?: boolean;
}): KbnClient => {
const url = spaceId ? buildUrlWithSpaceId(_url, spaceId) : _url;
const isHttps = new URL(url).protocol.startsWith('https');
const clientOptions: ConstructorParameters<typeof KbnClientExtended>[0] = {
log,
apiKey,
url: buildUrlWithCredentials(url, username, password),
};
if (isHttps && useCertForSsl) {
clientOptions.certificateAuthorities = [CA_CERTIFICATE];
}
if (log) {
log.verbose(
`Creating Kibana client with URL: ${clientOptions.url} ${
apiKey ? ` + ApiKey: ${apiKey}` : ''
}`
);
}
return new KbnClientExtended(clientOptions);
};
/**
* Builds a new URL based on the one provided on input for the given space id
* @param url
* @param spaceId
*/
export const buildUrlWithSpaceId = (url: string, spaceId: string): string => {
const newUrl = new URL(url);
let requestPath = newUrl.pathname;
const currentUrlSpace = getSpaceIdFromPath(requestPath); // NOTE: we are not currently supporting a Kibana base path prefix
if (currentUrlSpace.pathHasExplicitSpaceIdentifier) {
// Get the request path (if any) from the url
requestPath = requestPath.substring(`/s/${currentUrlSpace.spaceId}`.length) || '/';
}
newUrl.pathname = addSpaceIdToPath('/', spaceId, requestPath);
return newUrl.href;
};
/**
* Retrieves the Stack (kibana/ES) version from the `/api/status` kibana api
* @param kbnClient
*/
export const fetchStackVersion = async (kbnClient: KbnClient): Promise<string> => {
const status = await fetchKibanaStatus(kbnClient);
if (!status?.version?.number) {
throw new Error(
`unable to get stack version from '/api/status' \n${JSON.stringify(status, null, 2)}`
);
}
return status.version.number;
};
/**
* Checks to ensure Kibana is up and running
* @param kbnClient
*/
export const waitForKibana = async (kbnClient: KbnClient): Promise<void> => {
await pRetry(
async () => {
const response = await fetchKibanaStatus(kbnClient);
if (response.status.overall.level !== 'available') {
throw new Error(
`Kibana not available. [status.overall.level: ${response.status.overall.level}]`
);
}
},
{ maxTimeout: 10000 }
);
};

View file

@ -0,0 +1,34 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { schema, type TypeOf } from '@kbn/config-schema';
const TestFileFtrConfigSchema = schema.object(
{
license: schema.maybe(schema.string()),
kbnServerArgs: schema.maybe(schema.arrayOf(schema.string())),
productTypes: schema.maybe(
// TODO:PT write validate function to ensure that only the correct combinations are used
schema.arrayOf(
schema.object({
product_line: schema.oneOf([
schema.literal('security'),
schema.literal('endpoint'),
schema.literal('cloud'),
]),
product_tier: schema.oneOf([schema.literal('essentials'), schema.literal('complete')]),
})
)
),
},
{ defaultValue: {}, unknowns: 'forbid' }
);
export type SecuritySolutionDescribeBlockFtrConfig = TypeOf<typeof TestFileFtrConfigSchema>;

View file

@ -0,0 +1,34 @@
{
"extends": "../../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"cypress",
"jest",
"node",
]
},
"include": [
"cypress.d.ts",
"**/*.ts",
"src/**/*",
// have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636
"src/**/*.json",
"../../../../../typings/**/*",
],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/es",
"@kbn/security-plugin",
"@kbn/tooling-log",
"@kbn/repo-info",
"@kbn/test",
"@kbn/dev-cli-runner",
"@kbn/core-status-common",
"@kbn/dev-utils",
"@kbn/spaces-plugin",
"@kbn/config-schema",
]
}

View file

@ -728,6 +728,8 @@
"@kbn/custom-integrations-plugin/*": ["src/platform/plugins/shared/custom_integrations/*"],
"@kbn/cypress-config": ["src/platform/packages/shared/kbn-cypress-config"],
"@kbn/cypress-config/*": ["src/platform/packages/shared/kbn-cypress-config/*"],
"@kbn/cypress-test-helper": ["src/platform/packages/shared/kbn-cypress-test-helper"],
"@kbn/cypress-test-helper/*": ["src/platform/packages/shared/kbn-cypress-test-helper/*"],
"@kbn/dashboard-enhanced-plugin": ["x-pack/platform/plugins/shared/dashboard_enhanced"],
"@kbn/dashboard-enhanced-plugin/*": ["x-pack/platform/plugins/shared/dashboard_enhanced/*"],
"@kbn/dashboard-plugin": ["src/platform/plugins/shared/dashboard"],

View file

@ -10,7 +10,7 @@ import path from 'path';
import { load as loadYaml } from 'js-yaml';
import { readFileSync } from 'fs';
import type { YamlRoleDefinitions } from '@kbn/test-suites-serverless/shared/lib';
import { samlAuthentication } from '@kbn/security-solution-plugin/public/management/cypress/support/saml_authentication';
import { samlAuthentication } from '@kbn/cypress-test-helper/src/auth/saml_auth';
import { setupUserDataLoader } from './support/setup_data_loader_tasks';
import { getFailedSpecVideos } from './support/filter_videos';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { waitForAlertsToPopulate } from '@kbn/test-suites-xpack/security_solution_cypress/cypress/tasks/create_new_rule';
import { waitForAlertsToPopulate } from '@kbn/cypress-test-helper/src/services/alerting_services';
import { disableNewFeaturesTours } from '../../tasks/navigation';
import { initializeDataViews } from '../../tasks/login';
import { checkResults, clickRuleName, submitQuery } from '../../tasks/live_query';

View file

@ -31,8 +31,8 @@ import registerCypressGrep from '@cypress/grep';
registerCypressGrep();
import type { SecuritySolutionDescribeBlockFtrConfig } from '@kbn/security-solution-plugin/scripts/run_cypress/utils';
import { login } from '@kbn/security-solution-plugin/public/management/cypress/tasks/login';
import type { SecuritySolutionDescribeBlockFtrConfig } from '@kbn/cypress-test-helper/src/utils';
import { login } from '@kbn/cypress-test-helper/src/auth/login';
import type { LoadedRoleAndUser } from '@kbn/test-suites-serverless/shared/lib';
import type { ServerlessRoleName } from './roles';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { createRuntimeServices } from '@kbn/security-solution-plugin/scripts/endpoint/common/stack_services';
import { createRuntimeServices } from '@kbn/cypress-test-helper/src/services/stack_services';
import { SecurityRoleAndUserLoader } from '@kbn/test-suites-serverless/shared/lib';
import type {
LoadedRoleAndUser,

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { waitForAlertsToPopulate } from '@kbn/test-suites-xpack/security_solution_cypress/cypress/tasks/create_new_rule';
import { waitForAlertsToPopulate } from '@kbn/cypress-test-helper/src/services/alerting_services';
import { disableNewFeaturesTours } from './navigation';
import { getAdvancedButton } from '../screens/integrations';
import {

View file

@ -32,10 +32,8 @@
"path": "../tsconfig.json",
"force": true
},
"@kbn/security-solution-plugin",
"@kbn/fleet-plugin",
"@kbn/cases-plugin",
"@kbn/security-solution-plugin/public/management/cypress",
"@kbn/test-suites-xpack/security_solution_cypress/cypress",
"@kbn/cypress-test-helper",
]
}

View file

@ -5227,6 +5227,10 @@
version "0.0.0"
uid ""
"@kbn/cypress-test-helper@link:src/platform/packages/shared/kbn-cypress-test-helper":
version "0.0.0"
uid ""
"@kbn/dashboard-enhanced-plugin@link:x-pack/platform/plugins/shared/dashboard_enhanced":
version "0.0.0"
uid ""