From 49845c2bb868522fb3bac989d8e859bc0bb30c6a Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Wed, 2 Jul 2025 14:29:37 +0200 Subject: [PATCH 1/2] [ska] remove kbn/test-suites-xpack-security imports --- .github/CODEOWNERS | 1 + .../cloud_security_posture/benchmark/v1.ts | 2 +- .../cloud_security_posture/benchmark/v2.ts | 2 +- .../security/cloud_security_posture/data.ts | 148 ++++ .../find_csp_benchmark_rule.ts | 2 +- .../security/cloud_security_posture/graph.ts | 2 +- .../security/cloud_security_posture/helper.ts | 152 ++++ .../cloud_security_posture/mock_data.ts | 80 ++ .../cloud_security_metering.ts | 4 +- .../status/status_indexed.ts | 9 +- .../status/status_indexing.ts | 9 +- .../status_not_deployed_not_installed.ts | 2 +- .../cloud_security_posture/telemetry.ts | 9 +- .../security/cloud_security_posture/utils.ts | 93 +++ .../add_cis_integration_form_page.ts | 700 ++++++++++++++++++ .../alerts_page.ts | 121 +++ .../benchmark_page.ts | 116 +++ .../constants/test_subject_ids.ts | 80 ++ .../csp_dashboard_page.ts | 223 ++++++ .../expanded_flyout_graph.ts | 183 +++++ .../findings_page.ts | 399 ++++++++++ .../index.ts | 32 + .../network_events_page.ts | 121 +++ .../rule_page.ts | 253 +++++++ .../security_common.ts | 133 ++++ .../services/query_bar_provider.ts | 65 ++ .../timeline_page.ts | 103 +++ .../vulnerability_dashboard_page_object.ts | 129 ++++ .../functional/page_objects/index.ts | 2 +- 29 files changed, 3149 insertions(+), 26 deletions(-) create mode 100644 x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/data.ts create mode 100644 x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/helper.ts create mode 100644 x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/mock_data.ts create mode 100644 x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/utils.ts create mode 100644 x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/add_cis_integration_form_page.ts create mode 100644 x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/alerts_page.ts create mode 100644 x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/benchmark_page.ts create mode 100644 x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/constants/test_subject_ids.ts create mode 100644 x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/csp_dashboard_page.ts create mode 100644 x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/expanded_flyout_graph.ts create mode 100644 x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/findings_page.ts create mode 100644 x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/index.ts create mode 100644 x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/network_events_page.ts create mode 100644 x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/rule_page.ts create mode 100644 x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/security_common.ts create mode 100644 x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/services/query_bar_provider.ts create mode 100644 x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/timeline_page.ts create mode 100644 x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/vulnerability_dashboard_page_object.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5f2edeb27069b..61695bbf98df0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1885,6 +1885,7 @@ x-pack/platform/plugins/shared/ml/server/models/data_recognizer/modules/security /x-pack/solutions/security/test/api_integration/ftr_provider_context.d.ts @elastic/appex-qa /x-pack/solutions/security/test/api_integration/services/index.ts @elastic/appex-qa /x-pack/solutions/security/test/alerting_api_integration/ftr_provider_context.d.ts @elastic/appex-qa +/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects @elastic/appex-qa # temporarily due to SKA tests relocation # Core /src/platform/test/api_integration/fixtures/kbn_archiver/management/saved_objects/relationships.json @elastic/kibana-core @elastic/kibana-data-discovery diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/benchmark/v1.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/benchmark/v1.ts index bec22f86bd1f5..439501d8aa9fd 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/benchmark/v1.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/benchmark/v1.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import type { GetBenchmarkResponse } from '@kbn/cloud-security-posture-plugin/common/types/latest'; import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; -import { createPackagePolicy } from '@kbn/test-suites-xpack-security/api_integration/apis/cloud_security_posture/helper'; +import { createPackagePolicy } from '../helper'; import { FtrProviderContext } from '../../../../ftr_provider_context'; import { RoleCredentials } from '../../../../../shared/services'; diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/benchmark/v2.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/benchmark/v2.ts index c32ce2d21ec35..804e412f936ae 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/benchmark/v2.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/benchmark/v2.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import type { GetBenchmarkResponse } from '@kbn/cloud-security-posture-plugin/common/types/latest'; import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; -import { createPackagePolicy } from '@kbn/test-suites-xpack-security/api_integration/apis/cloud_security_posture/helper'; +import { createPackagePolicy } from '../helper'; import { FtrProviderContext } from '../../../../ftr_provider_context'; import { RoleCredentials } from '../../../../../shared/services'; diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/data.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/data.ts new file mode 100644 index 0000000000000..da50b2531e575 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/data.ts @@ -0,0 +1,148 @@ +/* + * 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. + */ + +export interface MockTelemetryFindings { + rule: { + benchmark: { id: string; posture_type?: string | undefined; version: string; name: string }; + }; + resource: { type: string; sub_type: string; id: string }; + agent: { id: string }; + result: { evaluation: string }; + host: { name: string }; + cluster_id?: string; + cloud?: { account?: { id: string } }; + cloudbeat?: { kubernetes: { version: string } }; +} + +export interface MockTelemetryData { + [key: string]: MockTelemetryFindings[]; +} + +export const data: MockTelemetryData = { + cspmFindings: [ + { + rule: { + benchmark: { + id: 'cis_aws', + posture_type: 'cspm', + version: 'v1.5.0', + name: 'CIS Amazon Web Services Foundations', + }, + }, + resource: { + type: 'identifyingType', + sub_type: 'aws-password-policy', + id: '15e450b7-8980-5bca-ade2-a0c795f9ea9d', + }, + agent: { id: '07bd3686-98ef-4b23-99cb-9ff544b25ae2' }, + result: { evaluation: 'failed' }, + cloud: { account: { id: 'my-aws-12345' } }, + host: { name: 'docker-fleet-agent' }, + }, + { + rule: { + benchmark: { + id: 'cis_aws', + posture_type: 'cspm', + version: 'v1.5.0', + name: 'CIS Amazon Web Services Foundations', + }, + }, + resource: { + type: 'identifyingType', + sub_type: 'aws-password-policy', + id: '15e450b7-8980-5bca-ade2-a0c795f9ea99', + }, + agent: { id: '07bd3686-98ef-4b23-99cb-9ff544b25ae2' }, + result: { evaluation: 'passed' }, + cloud: { account: { id: 'my-aws-12345' } }, + host: { name: 'docker-fleet-agent' }, + }, + ], + kspmFindings: [ + { + cluster_id: 'my-k8s-cluster-5555', + rule: { + benchmark: { + id: 'cis_k8s', + version: 'v1.0.0', + name: 'CIS Kubernetes V1.23', + posture_type: 'kspm', + }, + }, + resource: { + type: 'k8s_object', + sub_type: 'ServiceAccount', + id: '1111', + }, + agent: { id: '07bd3686-98ef-4b23-99cb-9ff544b25ae2' }, + result: { evaluation: 'passed' }, + host: { name: 'docker-fleet-agent' }, + cloudbeat: { kubernetes: { version: 'v1.23.0' } }, + }, + { + cluster_id: 'my-k8s-cluster-5555', + rule: { + benchmark: { + id: 'cis_k8s', + version: 'v1.0.0', + name: 'CIS Kubernetes V1.23', + posture_type: 'kspm', + }, + }, + resource: { + type: 'process', + sub_type: 'process', + id: '1111', + }, + agent: { id: '07bd3686-98ef-4b23-99cb-9ff544b25ae3' }, + result: { evaluation: 'passed' }, + host: { name: 'control-plane' }, + cloudbeat: { kubernetes: { version: 'v1.23.0' } }, + }, + ], + kspmFindingsNoPostureType: [ + { + cluster_id: 'my-k8s-cluster-5555', + rule: { + benchmark: { + id: 'cis_k8s', + version: 'v1.0.0', + name: 'CIS Kubernetes V1.23', + }, + }, + resource: { + type: 'k8s_object', + sub_type: 'ServiceAccount', + id: '1111', + }, + agent: { id: '07bd3686-98ef-4b23-99cb-9ff544b25ae2' }, + result: { evaluation: 'passed' }, + host: { name: 'docker-fleet-agent' }, + cloudbeat: { kubernetes: { version: 'v1.23.0' } }, + }, + { + cluster_id: 'my-k8s-cluster-5555', + rule: { + benchmark: { + id: 'cis_k8s', + version: 'v1.0.0', + name: 'CIS Kubernetes V1.23', + }, + }, + resource: { + type: 'process', + sub_type: 'process', + id: '1111', + }, + agent: { id: '07bd3686-98ef-4b23-99cb-9ff544b25ae3' }, + result: { evaluation: 'passed' }, + host: { name: 'control-plane' }, + cloudbeat: { kubernetes: { version: 'v1.23.0' } }, + }, + ], +}; diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/find_csp_benchmark_rule.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/find_csp_benchmark_rule.ts index 61d9713ba684d..6e42724e9c53a 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/find_csp_benchmark_rule.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/find_csp_benchmark_rule.ts @@ -11,7 +11,7 @@ import type { FindCspBenchmarkRuleResponse, } from '@kbn/cloud-security-posture-common/schema/rules/latest'; -import { createPackagePolicy } from '@kbn/test-suites-xpack-security/api_integration/apis/cloud_security_posture/helper'; +import { createPackagePolicy } from './helper'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { RoleCredentials } from '../../../../shared/services'; diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/graph.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/graph.ts index 2d096bbe9d9a5..ac8ffc222394c 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/graph.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/graph.ts @@ -10,9 +10,9 @@ import { ELASTIC_HTTP_VERSION_HEADER, X_ELASTIC_INTERNAL_ORIGIN_REQUEST, } from '@kbn/core-http-common'; -import { result } from '@kbn/test-suites-xpack-security/cloud_security_posture_api/utils'; import type { Agent } from 'supertest'; import type { GraphRequest } from '@kbn/cloud-security-posture-common/types/graph/v1'; +import { result } from './utils'; import type { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/helper.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/helper.ts new file mode 100644 index 0000000000000..01d948ecc7500 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/helper.ts @@ -0,0 +1,152 @@ +/* + * 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 type { Agent as SuperTestAgent } from 'supertest'; + +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import { CLOUD_SECURITY_PLUGIN_VERSION } from '@kbn/cloud-security-posture-plugin/common/constants'; +import { RoleCredentials, SecurityService } from '@kbn/ftr-common-functional-services'; +import { SECURITY_FEATURE_ID } from '@kbn/security-solution-plugin/common/constants'; + +export async function createPackagePolicy( + supertest: SuperTestAgent, + agentPolicyId: string, + policyTemplate: string, + input: string, + deployment: string, + posture: string, + packageName: string = 'cloud_security_posture-1', + roleAuthc?: RoleCredentials, + internalRequestHeader?: { 'x-elastic-internal-origin': string; 'kbn-xsrf': string } +) { + const version = CLOUD_SECURITY_PLUGIN_VERSION; + const title = 'Security Posture Management'; + const streams = [ + { + enabled: true, + data_stream: { + type: 'logs', + dataset: 'cloud_security_posture.vulnerabilities', + }, + }, + ]; + + const inputTemplate = { + enabled: true, + type: input, + policy_template: policyTemplate, + }; + + const inputs = posture === 'vuln_mgmt' ? { ...inputTemplate, streams } : { ...inputTemplate }; + + const { body: postPackageResponse } = + roleAuthc && internalRequestHeader + ? await supertest + .post(`/api/fleet/package_policies`) + .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') + .set(internalRequestHeader) + .set(roleAuthc.apiKeyHeader) + .send({ + force: true, + name: packageName, + description: '', + namespace: 'default', + policy_id: agentPolicyId, + enabled: true, + inputs: [inputs], + package: { + name: 'cloud_security_posture', + title, + version, + }, + vars: { + deployment: { + value: deployment, + type: 'text', + }, + posture: { + value: posture, + type: 'text', + }, + }, + }) + .expect(200) + : await supertest + .post(`/api/fleet/package_policies`) + .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') + .set('kbn-xsrf', 'xxxx') + .send({ + force: true, + name: packageName, + description: '', + namespace: 'default', + policy_id: agentPolicyId, + enabled: true, + inputs: [inputs], + package: { + name: 'cloud_security_posture', + title, + version, + }, + vars: { + deployment: { + value: deployment, + type: 'text', + }, + posture: { + value: posture, + type: 'text', + }, + }, + }) + .expect(200); + + return postPackageResponse.item; +} + +export const createUser = async (security: SecurityService, userName: string, roleName: string) => { + await security.user.create(userName, { + password: 'changeme', + roles: [roleName], + full_name: 'a reporting user', + }); +}; + +export const createCSPRole = async ( + security: SecurityService, + roleName: string, + indicesName?: string[] +) => { + await security.role.create(roleName, { + kibana: [ + { + feature: { [SECURITY_FEATURE_ID]: ['read'], fleetv2: ['all'], fleet: ['read'] }, + spaces: ['*'], + }, + ], + ...(indicesName && indicesName.length > 0 + ? { + elasticsearch: { + indices: [ + { + names: indicesName, + privileges: ['read'], + }, + ], + }, + } + : {}), + }); +}; + +export const deleteRole = async (security: SecurityService, roleName: string) => { + await security.role.delete(roleName); +}; + +export const deleteUser = async (security: SecurityService, userName: string) => { + await security.user.delete(userName); +}; diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/mock_data.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/mock_data.ts new file mode 100644 index 0000000000000..beb425c2318ea --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/mock_data.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import Chance from 'chance'; + +const chance = new Chance(); + +export const findingsMockData = [ + { + resource: { id: chance.guid(), name: `kubelet`, sub_type: 'lower case sub type' }, + result: { evaluation: chance.integer() % 2 === 0 ? 'passed' : 'failed' }, + rule: { + name: 'Upper case rule name', + section: 'Upper case section', + benchmark: { + id: 'cis_k8s', + posture_type: 'kspm', + name: 'CIS Kubernetes V1.23', + version: 'v1.0.0', + }, + type: 'process', + }, + cluster_id: 'Upper case cluster id', + event: { + ingested: '2023-08-19T18:20:41Z', + created: '2023-08-19T18:17:15.609124281Z', + }, + data_stream: { + dataset: 'cloud_security_posture.findings', + }, + }, + { + resource: { id: chance.guid(), name: `Pod`, sub_type: 'Upper case sub type' }, + result: { evaluation: chance.integer() % 2 === 0 ? 'passed' : 'failed' }, + rule: { + name: 'lower case rule name', + section: 'Another upper case section', + benchmark: { + id: 'cis_aws', + posture_type: 'cspm', + name: 'CIS Kubernetes V1.23', + version: 'v1.0.0', + }, + type: 'process', + }, + cluster_id: 'Another Upper case cluster id', + event: { + ingested: '2023-08-19T18:20:41Z', + created: '2023-08-19T18:17:15.609124281Z', + }, + data_stream: { + dataset: 'cloud_security_posture.findings', + }, + }, +]; + +export const vulnerabilityMockData = [ + { + resource: { + name: 'NameNama', + id: '12345', + }, + vulnerability: { + severity: 'MEDIUM', + package: { + name: 'github.com/aws/aws-sdk-go', + version: 'v1.42.30', + }, + }, + cvss: { + redhat: { + V3Vector: 'CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:C/C:H/I:N/A:N', + V3Score: 5.6, + }, + }, + }, +]; diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/cloud_security_metering.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/cloud_security_metering.ts index eaffae523c263..ace0b266e606c 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/cloud_security_metering.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/serverless_metering/cloud_security_metering.ts @@ -11,8 +11,8 @@ import { CDR_LATEST_NATIVE_MISCONFIGURATIONS_INDEX_ALIAS, } from '@kbn/cloud-security-posture-common'; import * as http from 'http'; -import { createPackagePolicy } from '@kbn/test-suites-xpack-security/api_integration/apis/cloud_security_posture/helper'; -import { EsIndexDataProvider } from '@kbn/test-suites-xpack-security/cloud_security_posture_api/utils'; +import { createPackagePolicy } from '../helper'; +import { EsIndexDataProvider } from '../utils'; import { RoleCredentials } from '../../../../../shared/services'; import { getMockFindings } from './mock_data'; import type { FtrProviderContext } from '../../../../ftr_provider_context'; diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/status/status_indexed.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/status/status_indexed.ts index b24e25c76540f..9096ce7c9d377 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/status/status_indexed.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/status/status_indexed.ts @@ -11,12 +11,9 @@ import { CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN, CDR_LATEST_NATIVE_MISCONFIGURATIONS_INDEX_ALIAS, } from '@kbn/cloud-security-posture-common'; -import { createPackagePolicy } from '@kbn/test-suites-xpack-security/api_integration/apis/cloud_security_posture/helper'; -import { EsIndexDataProvider } from '@kbn/test-suites-xpack-security/cloud_security_posture_api/utils'; -import { - findingsMockData, - vulnerabilityMockData, -} from '@kbn/test-suites-xpack-security/api_integration/apis/cloud_security_posture/mock_data'; +import { createPackagePolicy } from '../helper'; +import { EsIndexDataProvider } from '../utils'; +import { findingsMockData, vulnerabilityMockData } from '../mock_data'; import { FtrProviderContext } from '../../../../ftr_provider_context'; import { RoleCredentials } from '../../../../../shared/services'; diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/status/status_indexing.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/status/status_indexing.ts index c256a9ae3da7f..3a3ca88b72751 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/status/status_indexing.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/status/status_indexing.ts @@ -11,12 +11,9 @@ import { FINDINGS_INDEX_DEFAULT_NS, VULNERABILITIES_INDEX_DEFAULT_NS, } from '@kbn/cloud-security-posture-plugin/common/constants'; -import { createPackagePolicy } from '@kbn/test-suites-xpack-security/api_integration/apis/cloud_security_posture/helper'; -import { - findingsMockData, - vulnerabilityMockData, -} from '@kbn/test-suites-xpack-security/api_integration/apis/cloud_security_posture/mock_data'; -import { EsIndexDataProvider } from '@kbn/test-suites-xpack-security/cloud_security_posture_api/utils'; +import { createPackagePolicy } from '../helper'; +import { findingsMockData, vulnerabilityMockData } from '../mock_data'; +import { EsIndexDataProvider } from '../utils'; import { FtrProviderContext } from '../../../../ftr_provider_context'; import { RoleCredentials } from '../../../../../shared/services'; diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/status/status_not_deployed_not_installed.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/status/status_not_deployed_not_installed.ts index f30e1cc07f8b2..4b90f3bd8c83e 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/status/status_not_deployed_not_installed.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/status/status_not_deployed_not_installed.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import type { CspSetupStatus } from '@kbn/cloud-security-posture-common'; import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; -import { createPackagePolicy } from '@kbn/test-suites-xpack-security/api_integration/apis/cloud_security_posture/helper'; +import { createPackagePolicy } from '../helper'; import { FtrProviderContext } from '../../../../ftr_provider_context'; import { RoleCredentials } from '../../../../../shared/services'; diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/telemetry.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/telemetry.ts index 04578e826320d..4f5a4a24eb853 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/telemetry.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/telemetry.ts @@ -7,12 +7,9 @@ import expect from '@kbn/expect'; import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; -import { data as telemetryMockData } from '@kbn/test-suites-xpack-security/cloud_security_posture_api/telemetry/data'; -import { createPackagePolicy } from '@kbn/test-suites-xpack-security/api_integration/apis/cloud_security_posture/helper'; -import { - waitForPluginInitialized, - EsIndexDataProvider, -} from '@kbn/test-suites-xpack-security/cloud_security_posture_api/utils'; +import { data as telemetryMockData } from './data'; +import { createPackagePolicy } from './helper'; +import { waitForPluginInitialized, EsIndexDataProvider } from './utils'; import { SupertestWithRoleScopeType } from '../../../services'; import type { FtrProviderContext } from '../../../ftr_provider_context'; import { RoleCredentials } from '../../../../shared/services'; diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/utils.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/utils.ts new file mode 100644 index 0000000000000..210a081b91473 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/utils.ts @@ -0,0 +1,93 @@ +/* + * 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 type { RetryService } from '@kbn/ftr-common-functional-services'; +import type { Agent } from 'supertest'; +import type { ToolingLog } from '@kbn/tooling-log'; +import type { Client as EsClient } from '@elastic/elasticsearch'; +import type { CallbackHandler, Response } from 'superagent'; +import expect from '@kbn/expect'; +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; + +/** + * Checks if plugin initialization was done + * Required before indexing findings + */ +export const waitForPluginInitialized = ({ + retry, + logger, + supertest, +}: { + retry: RetryService; + logger: ToolingLog; + supertest: Pick; +}): Promise => + retry.try(async () => { + logger.debug('Check CSP plugin is initialized'); + const response = await supertest + .get('/internal/cloud_security_posture/status?check=init') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .expect(200); + expect(response.body).to.eql({ isPluginInitialized: true }); + logger.debug('CSP plugin is initialized'); + }); + +export function result(status: number, logger?: ToolingLog): CallbackHandler { + return (err: any, res: Response) => { + if ((res?.status || err.status) !== status) { + throw new Error( + `Expected ${status} ,got ${res?.status || err.status} resp: ${ + res?.body ? JSON.stringify(res.body) : err.text + }` + ); + } else if (err) { + logger?.warning(`Error result ${err.text}`); + } + }; +} + +export class EsIndexDataProvider { + private es: EsClient; + private readonly index: string; + + constructor(es: EsClient, index: string) { + this.es = es; + this.index = index; + } + + async addBulk(docs: Array>, overrideTimestamp = true) { + const operations = docs.flatMap((doc) => [ + { create: { _index: this.index } }, + { ...doc, ...(overrideTimestamp ? { '@timestamp': new Date().toISOString() } : {}) }, + ]); + + const resp = await this.es.bulk({ refresh: 'wait_for', index: this.index, operations }); + expect(resp.errors).eql(false, `Error in bulk indexing: ${JSON.stringify(resp)}`); + + return resp; + } + + async deleteAll() { + const indexExists = await this.es.indices.exists({ index: this.index }); + + if (indexExists) { + return this.es.deleteByQuery({ + index: this.index, + query: { match_all: {} }, + refresh: true, + }); + } + } + + async destroyIndex() { + const indexExists = await this.es.indices.exists({ index: this.index }); + + if (indexExists) { + return this.es.indices.delete({ index: this.index }); + } + } +} diff --git a/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/add_cis_integration_form_page.ts b/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/add_cis_integration_form_page.ts new file mode 100644 index 0000000000000..7b78eacd5d996 --- /dev/null +++ b/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/add_cis_integration_form_page.ts @@ -0,0 +1,700 @@ +/* + * 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 { v4 as uuidv4 } from 'uuid'; +import expect from '@kbn/expect'; +import { testSubjectIds } from './constants/test_subject_ids'; +import type { FtrProviderContext } from '../../ftr_provider_context'; + +const TEST_IDS = { + POST_INSTALL_AZURE_ARM_TEMPLATE_MODAL: 'postInstallAzureArmTemplateModal', + EXTERNAL_LINK: 'externalLink', + POST_INSTALL_CLOUD_FORMATION_MODAL: 'postInstallCloudFormationModal', + LAUNCH_CLOUD_FORMATION_AGENTLESS_BUTTON: 'launchCloudFormationAgentlessButton', + POST_INSTALL_GOOGLE_CLOUD_SHELL_MODAL: 'postInstallGoogleCloudShellModal', + DATA_COLLECTION_SETUP_STEP: 'dataCollectionSetupStep', + INTEGRATION_NAME_LINK: 'integrationNameLink', + AGENT_ENROLLMENT_FLYOUT: 'agentEnrollmentFlyout', + AGENTLESS_INTEGRATION_NAME_LINK: 'agentlessIntegrationNameLink', + INTEGRATION_POLICY_TABLE: 'integrationPolicyTable', + EDIT_PACKAGE_POLICY_PAGE: 'editPackagePolicy_page', + CREATE_PACKAGE_POLICY_PAGE: 'createPackagePolicy_page', + CREATE_PACKAGE_POLICY_SAVE_BUTTON: 'createPackagePolicySaveButton', + CONFIRM_CLOUD_FORMATION_MODAL_CONFIRM_BUTTON: 'confirmCloudFormationModalConfirmButton', + SAVE_INTEGRATION: 'saveIntegration', + CONFIRM_MODAL_TITLE_TEXT: 'confirmModalTitleText', + CLOUD_SECURITY_POSTURE_PLI_AUTH_BLOCK: 'cloud-security-posture-integration-pli-auth-block', + ADD_AGENT_BUTTON: 'addAgentButton', + POLICY_UPDATE_SUCCESS_TOAST: 'policyUpdateSuccessToast', + AGENT_POLICY_NAME_LINK: 'agentPolicyNameLink', + AGENTLESS_STATUS_BADGE: 'agentlessStatusBadge', + CREATE_AGENT_POLICY_NAME_FIELD: 'createAgentPolicyNameField', + CREDENTIALS_JSON_SECRET_PANEL: 'credentials_json_secret_panel_test_id', + GCP_POLICY_OPTION_TEST_ID: 'cisGcpTestId', + AWS_POLICY_OPTION_TEST_ID: 'cisAwsTestId', + AZURE_POLICY_OPTION_TEST_ID: 'cisAzureTestId', +} as const; + +export function AddCisIntegrationFormPageProvider({ + getService, + getPageObjects, +}: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'header']); + const browser = getService('browser'); + const logger = getService('log'); + const retry = getService('retry'); + + const AWS_CREDENTIAL_SELECTOR = 'aws-credentials-type-selector'; + + const cisAzure = { + getPostInstallArmTemplateModal: async () => { + return await testSubjects.find(TEST_IDS.POST_INSTALL_AZURE_ARM_TEMPLATE_MODAL); + }, + }; + + const cisAws = { + getUrlValueInEditPage: async () => { + /* Newly added/edited integration always shows up on top by default as such we can just always click the most top if we want to check for the latest one */ + const fieldValue = await ( + await testSubjects.find(TEST_IDS.EXTERNAL_LINK) + ).getAttribute('href'); + return fieldValue; + }, + + getPostInstallCloudFormationModal: async () => { + return await testSubjects.find(TEST_IDS.POST_INSTALL_CLOUD_FORMATION_MODAL); + }, + showPostInstallCloudFormationModal: async () => { + return await testSubjects.exists(TEST_IDS.POST_INSTALL_CLOUD_FORMATION_MODAL); + }, + showLaunchCloudFormationAgentlessButton: async () => { + return await testSubjects.exists(TEST_IDS.LAUNCH_CLOUD_FORMATION_AGENTLESS_BUTTON); + }, + }; + + const cisGcp = { + isPostInstallGoogleCloudShellModal: async (isOrg: boolean, orgID?: string, prjID?: string) => { + const googleCloudShellModal = await testSubjects.find( + TEST_IDS.POST_INSTALL_GOOGLE_CLOUD_SHELL_MODAL + ); + const googleCloudShellModalVisibleText = await googleCloudShellModal.getVisibleText(); + const stringProjectId = prjID ? prjID : ''; + const stringOrganizationId = orgID ? `ORG_ID=${orgID}` : 'ORG_ID='; + const orgIdExist = googleCloudShellModalVisibleText.includes(stringOrganizationId); + const prjIdExist = googleCloudShellModalVisibleText.includes(stringProjectId); + + if (isOrg) { + return orgIdExist === true && prjIdExist === true; + } else { + return orgIdExist === false && prjIdExist === true; + } + }, + + checkGcpFieldExist: async (text: string) => { + const field = await testSubjects.findAll(text); + return field.length; + }, + + fillInTextField: async (selector: string, text: string) => { + const textField = await testSubjects.find(selector); + await textField.type(text); + }, + + chooseDropDown: async (selector: string, text: string) => { + const credentialTypeBox = await testSubjects.find(selector); + const chosenOption = await testSubjects.find(text); + await credentialTypeBox.click(); + await chosenOption.click(); + }, + + getFieldValueInEditPage: async (field: string) => { + /* Newly added/edited integration always shows up on top by default as such we can just always click the most top if we want to check for the latest one */ + const integrationList = await testSubjects.findAll(TEST_IDS.INTEGRATION_NAME_LINK); + await integrationList[0].click(); + const fieldValue = await (await testSubjects.find(field)).getAttribute('value'); + return fieldValue; + }, + + doesStringExistInCodeBlock: async (str: string) => { + const flyout = await testSubjects.find(TEST_IDS.AGENT_ENROLLMENT_FLYOUT); + const codeBlock = await flyout.findByXpath('//code'); + const commandsToBeCopied = await codeBlock.getVisibleText(); + return commandsToBeCopied.includes(str); + }, + + getFieldValueInAddAgentFlyout: async (field: string, value: string) => { + /* Newly added/edited integration always shows up on top by default as such we can just always click the most top if we want to check for the latest one */ + const integrationList = await testSubjects.findAll(TEST_IDS.AGENT_ENROLLMENT_FLYOUT); + await integrationList[0].click(); + await PageObjects.header.waitUntilLoadingHasFinished(); + const fieldValue = (await (await testSubjects.find(field)).getAttribute(value)) ?? ''; + return fieldValue; + }, + showLaunchCloudShellAgentlessButton: async () => { + return await testSubjects.exists('launchGoogleCloudShellAgentlessButton'); + }, + }; + + const isRadioButtonChecked = async (selector: string) => { + const page = await testSubjects.find(TEST_IDS.DATA_COLLECTION_SETUP_STEP); + const findCheckedButton = await page.findAllByCssSelector(`input[id="${selector}"]:checked`); + if (findCheckedButton.length === 0) return false; + return true; + }; + + const getUrlOnPostInstallModal = async () => { + /* Newly added/edited integration always shows up on top by default as such we can just always click the most top if we want to check for the latest one */ + const fieldValue = await (await testSubjects.find(TEST_IDS.EXTERNAL_LINK)).getAttribute('href'); + return fieldValue; + }; + + const navigateToAddIntegrationCspmPage = async (space?: string) => { + const options = space + ? { + basePath: `/s/${space}`, + shouldUseHashForSubUrl: false, + } + : { + shouldUseHashForSubUrl: false, + }; + + await PageObjects.common.navigateToUrl( + 'fleet', // Defined in Security Solution plugin + 'integrations/cloud_security_posture/add-integration/cspm', + options + ); + await PageObjects.header.waitUntilLoadingHasFinished(); + }; + + const navigateToAddIntegrationWithVersionPage = async ( + packageVersion: string, + space?: string + ) => { + const options = space + ? { + basePath: `/s/${space}`, + shouldUseHashForSubUrl: false, + } + : { + shouldUseHashForSubUrl: false, + }; + + await PageObjects.common.navigateToUrl( + 'fleet', + `integrations/cloud_security_posture-${packageVersion}/add-integration`, + options + ); + await PageObjects.header.waitUntilLoadingHasFinished(); + }; + + const navigateToAddIntegrationCspmWithVersionPage = async ( + packageVersion: string, + space?: string + ) => { + const options = space + ? { + basePath: `/s/${space}`, + shouldUseHashForSubUrl: false, + } + : { + shouldUseHashForSubUrl: false, + }; + + await PageObjects.common.navigateToUrl( + 'fleet', + `integrations/cloud_security_posture-${packageVersion}/add-integration/cspm`, + options + ); + await PageObjects.header.waitUntilLoadingHasFinished(); + }; + + const navigateToAddIntegrationCnvmPage = async (space?: string) => { + const options = space + ? { + basePath: `/s/${space}`, + shouldUseHashForSubUrl: false, + } + : { + shouldUseHashForSubUrl: false, + }; + + await PageObjects.common.navigateToUrl( + 'fleet', // Defined in Security Solution plugin + 'integrations/cloud_security_posture/add-integration/vuln_mgmt', + options + ); + await PageObjects.header.waitUntilLoadingHasFinished(); + }; + + const navigateToEditIntegrationPage = async () => { + await testSubjects.click(TEST_IDS.INTEGRATION_NAME_LINK); + }; + + const navigateToEditAgentlessIntegrationPage = async () => { + await testSubjects.click(TEST_IDS.AGENTLESS_INTEGRATION_NAME_LINK); + }; + + const navigateToAddIntegrationKspmPage = async (space?: string) => { + const options = space + ? { + basePath: `/s/${space}`, + shouldUseHashForSubUrl: false, + } + : { + shouldUseHashForSubUrl: false, + }; + + await PageObjects.common.navigateToUrl( + 'fleet', // Defined in Security Solution plugin + 'integrations/cloud_security_posture/add-integration/kspm', + options + ); + await PageObjects.header.waitUntilLoadingHasFinished(); + }; + + const navigateToIntegrationCspList = async () => { + await PageObjects.common.navigateToActualUrl( + 'integrations', // Defined in Security Solution plugin + '/detail/cloud_security_posture/policies', + { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await PageObjects.header.waitUntilLoadingHasFinished(); + }; + + const clickPolicyToBeEdited = async (name: string) => { + const table = await testSubjects.find(TEST_IDS.INTEGRATION_POLICY_TABLE); + const integrationToBeEdited = await table.findByXpath(`//text()="${name}"`); + await integrationToBeEdited.click(); + }; + + const clickFirstElementOnIntegrationTable = async () => { + const integrationList = await testSubjects.findAll(TEST_IDS.INTEGRATION_NAME_LINK); + await integrationList[0].click(); + }; + + const clickFirstElementOnIntegrationTableAddAgent = async () => { + const integrationList = await testSubjects.exists(TEST_IDS.ADD_AGENT_BUTTON); + if (integrationList) { + await testSubjects.click(TEST_IDS.ADD_AGENT_BUTTON); + } + }; + + const clickLaunchAndGetCurrentUrl = async (buttonId: string) => { + const button = await testSubjects.find(buttonId); + await button.click(); + // Wait a bit to allow the new tab to load the URL + await retry.tryForTime(3000, async () => { + await browser.switchTab(1); + }); + const currentUrl = await browser.getCurrentUrl(); + await browser.closeCurrentWindow(); + await browser.switchTab(0); + return currentUrl; + }; + + const getIntegrationFormEntirePage = () => testSubjects.find(TEST_IDS.DATA_COLLECTION_SETUP_STEP); + + const getIntegrationPolicyTable = () => testSubjects.find(TEST_IDS.INTEGRATION_POLICY_TABLE); + + const getIntegrationFormEditPage = () => testSubjects.find(TEST_IDS.EDIT_PACKAGE_POLICY_PAGE); + + const findOptionInPage = async (text: string) => { + await PageObjects.header.waitUntilLoadingHasFinished(); + const optionToBeClicked = await testSubjects.find(text); + return await optionToBeClicked; + }; + + const clickAccordianButton = async (text: string) => { + await PageObjects.header.waitUntilLoadingHasFinished(); + const advancedAccordian = await testSubjects.find(text); + await advancedAccordian.scrollIntoView(); + await advancedAccordian.click(); + }; + + const selectSetupTechnology = async (setupTechnology: 'agentless' | 'agent-based') => { + const radioGroup = await testSubjects.find(testSubjectIds.SETUP_TECHNOLOGY_SELECTOR); + const radio = await radioGroup.findByCssSelector(`input[value='${setupTechnology}']`); + await radio.click(); + }; + + const getSetupTechnologyRadio = async (setupTechnology: 'agentless' | 'agent-based') => { + const radioGroup = await testSubjects.find(testSubjectIds.SETUP_TECHNOLOGY_SELECTOR); + return await radioGroup.findByCssSelector(`input[value='${setupTechnology}']`); + }; + + const showSetupTechnologyComponent = async () => { + return await testSubjects.exists(testSubjectIds.SETUP_TECHNOLOGY_SELECTOR); + }; + + const selectAwsCredentials = async ( + credentialType: 'direct' | 'temporary' | 'cloud_connectors' + ) => { + let credentialTypeValue = 'direct_access_keys'; + + if (credentialType === 'temporary') { + credentialTypeValue = 'temporary_keys'; + } + if (credentialType === 'cloud_connectors') { + credentialTypeValue = 'cloud_connector'; + } + await testSubjects.click(AWS_CREDENTIAL_SELECTOR); + await selectValue(AWS_CREDENTIAL_SELECTOR, credentialTypeValue); + }; + + const clickOptionButton = async (text: string) => { + const optionToBeClicked = await findOptionInPage(text); + await optionToBeClicked.scrollIntoView(); + await optionToBeClicked.click(); + }; + + const isSaveButtonEnabled = async () => { + const saveButton = await testSubjects.find(TEST_IDS.CREATE_PACKAGE_POLICY_SAVE_BUTTON); + const isEnabled = await saveButton.getAttribute('disabled'); + return isEnabled === null; // If the button is enabled, it won't have a 'disabled' attribute + }; + + const clickAwsPolicyOption = async () => { + const awsPolicyOption = await findOptionInPage(TEST_IDS.AWS_POLICY_OPTION_TEST_ID); + await awsPolicyOption.click(); + }; + + const clickGcpPolicyOption = async () => { + const gcpPolicyOption = await findOptionInPage(TEST_IDS.GCP_POLICY_OPTION_TEST_ID); + await gcpPolicyOption.click(); + }; + + const clickAzurePolicyOption = async () => { + const azurePolicyOption = await findOptionInPage(TEST_IDS.AZURE_POLICY_OPTION_TEST_ID); + await azurePolicyOption.click(); + }; + + const clickSaveButton = async () => { + const optionToBeClicked = await findOptionInPage(TEST_IDS.CREATE_PACKAGE_POLICY_SAVE_BUTTON); + await optionToBeClicked.click(); + }; + + const waitUntilLaunchCloudFormationButtonAppears = async () => + await testSubjects.exists(TEST_IDS.CONFIRM_CLOUD_FORMATION_MODAL_CONFIRM_BUTTON); + + const clickSaveIntegrationButton = async () => { + const optionToBeClicked = await findOptionInPage(TEST_IDS.SAVE_INTEGRATION); + await optionToBeClicked.click(); + }; + + const getPostInstallModal = async () => { + return await testSubjects.exists(TEST_IDS.CONFIRM_MODAL_TITLE_TEXT); + }; + + const checkIntegrationPliAuthBlockExists = async () => { + return await testSubjects.exists(TEST_IDS.CLOUD_SECURITY_POSTURE_PLI_AUTH_BLOCK); + }; + + const fillInTextField = async (selector: string, text: string) => { + const textField = await testSubjects.find(selector); + await textField.clearValueWithKeyboard(); + await textField.type(text); + }; + + const fillInComboBox = async (selector: string, text: string) => { + const comboBox = await testSubjects.find(selector); + // Click to open the combobox + await comboBox.click(); + // Find the input within the combobox + const input = await comboBox.findByCssSelector('input'); + // Clear and type new value + await input.clearValueWithKeyboard(); + await input.type(text); + // Press Enter to create the custom option + await input.pressKeys(browser.keys.ENTER); + }; + + const chooseDropDown = async (selector: string, text: string) => { + const credentialTypeBox = await testSubjects.find(selector); + const chosenOption = await testSubjects.find(text); + await credentialTypeBox.click(); + await chosenOption.click(); + }; + + const doesStringExistInCodeBlock = async (str: string) => { + const flyout = await testSubjects.find(TEST_IDS.AGENT_ENROLLMENT_FLYOUT); + const codeBlock = await flyout.findByXpath('//code'); + const commandsToBeCopied = await codeBlock.getVisibleText(); + return commandsToBeCopied.includes(str); + }; + + const getFieldValueInAddAgentFlyout = async (field: string, value: string) => { + /* Newly added/edited integration always shows up on top by default as such we can just always click the most top if we want to check for the latest one */ + const integrationList = await testSubjects.findAll(TEST_IDS.AGENT_ENROLLMENT_FLYOUT); + await integrationList[0].click(); + await PageObjects.header.waitUntilLoadingHasFinished(); + const fieldValue = await (await testSubjects.find(field)).getAttribute(value); + return fieldValue; + }; + + const selectValue = async (selector: string, value: string) => { + return testSubjects.selectValue(selector, value); + }; + + const getValueInEditPage = async (field: string) => { + /* Newly added/edited integration always shows up on top by default as such we can just always click the most top if we want to check for the latest one */ + const fieldValue = await (await testSubjects.find(field)).getAttribute('value'); + return fieldValue; + }; + + const isOptionChecked = async (testId: string, id: string) => { + const checkBox = await testSubjects.find(testId); + return await (await checkBox.findByCssSelector(`input[id='${id}']`)).getAttribute('checked'); + }; + + const getReplaceSecretButton = async (secretField: string) => { + return await testSubjects.find(`button-replace-${secretField}`); + }; + + const showCredentialJsonSecretPanel = async () => { + return await testSubjects.exists(TEST_IDS.CREDENTIALS_JSON_SECRET_PANEL); + }; + + const inputUniqueIntegrationName = async () => { + const flyout = await testSubjects.find(TEST_IDS.CREATE_PACKAGE_POLICY_PAGE); + const nameField = await flyout.findAllByCssSelector('input[id="name"]'); + await nameField[0].type(uuidv4()); + }; + + const inputIntegrationName = async (text: string) => { + const page = await testSubjects.find(TEST_IDS.CREATE_PACKAGE_POLICY_PAGE); + const nameField = await page.findAllByCssSelector('input[id="name"]'); + await nameField[0].clearValueWithKeyboard(); + await nameField[0].type(text); + }; + + const getSecretComponentReplaceButton = async (secretButtonSelector: string) => { + const secretComponentReplaceButton = await testSubjects.find(secretButtonSelector); + return secretComponentReplaceButton; + }; + + const getElementText = async (selector: string) => { + const element = await testSubjects.find(selector); + const text = await element.getVisibleText(); + return text; + }; + + const getFieldAttributeValue = async (field: string, attribute: string) => { + const fieldValue = await (await testSubjects.find(field)).getAttribute(attribute); + return fieldValue; + }; + + const getFieldValueInEditPage = async (field: string) => { + /* Newly added/edited integration always shows up on top by default as such we can just always click the most top if we want to check for the latest one */ + await navigateToEditIntegrationPage(); + const fieldValue = await getFieldAttributeValue(field, 'value'); + return fieldValue; + }; + + const fillOutAWSForm = async () => { + const directAccessKeyId = 'directAccessKeyIdTest'; + const directAccessSecretKey = 'directAccessSecretKeyTest'; + + await clickOptionButton(testSubjectIds.CIS_AWS_OPTION_TEST_ID); + + await selectSetupTechnology('agentless'); + await selectValue(testSubjectIds.AWS_CREDENTIAL_SELECTOR, 'direct_access_keys'); + await fillInTextField(testSubjectIds.DIRECT_ACCESS_KEY_ID_TEST_ID, directAccessKeyId); + await fillInTextField(testSubjectIds.DIRECT_ACCESS_SECRET_KEY_TEST_ID, directAccessSecretKey); + }; + + const fillOutGCPForm = async () => { + const projectId = 'PRJ_NAME_TEST'; + const credentialJson = 'CRED_JSON_TEST_NAME'; + + await clickOptionButton(testSubjectIds.CIS_GCP_OPTION_TEST_ID); + await clickOptionButton(testSubjectIds.GCP_SINGLE_ACCOUNT_TEST_ID); + await selectSetupTechnology('agentless'); + await fillInTextField(testSubjectIds.PRJ_ID_TEST_ID, projectId); + await fillInTextField(testSubjectIds.CREDENTIALS_JSON_TEST_ID, credentialJson); + }; + + const fillOutForm = async (cloudProvider: 'aws' | 'gcp') => { + switch (cloudProvider) { + case 'aws': + await fillOutAWSForm(); + break; + case 'gcp': + await fillOutGCPForm(); + break; + } + }; + + const createAgentlessIntegration = async ({ + cloudProvider, + }: { + cloudProvider: 'aws' | 'gcp'; + }) => { + // Navigate to the Integration CSPM to create an agentless integration + await navigateToAddIntegrationCspmPage(); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await inputIntegrationName(`cloud_security_posture-${new Date().toISOString()}`); + + await fillOutForm(cloudProvider); + + // Click Save Button to create the Integration then navigate to Integration Policies Tab Page + await clickSaveButton(); + await PageObjects.header.waitUntilLoadingHasFinished(); + }; + + const editAgentlessIntegration = async (testSubjectId: string, value: string) => { + await navigateToIntegrationCspList(); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await navigateToEditAgentlessIntegrationPage(); + await PageObjects.header.waitUntilLoadingHasFinished(); + + // Fill out form to edit an agentless integration + await fillInTextField(testSubjectId, value); + + // TechDebt: This is a workaround to ensure the form is saved + // const agentlessRadio = await getSetupTechnologyRadio('agentless'); + // const agentBasedRadio = await getSetupTechnologyRadio('agent-based'); + // expect(await agentlessRadio.isEnabled()).to.be(true); + // expect(agentBasedRadio.isEnabled()).to.be(true); + + // Clicking Save Button updates and navigates to Integration Policies Tab Page + await clickSaveIntegrationButton(); + await PageObjects.header.waitUntilLoadingHasFinished(); + + // Check if the Direct Access Key is updated package policy api with successful toast + expect(await testSubjects.exists(TEST_IDS.POLICY_UPDATE_SUCCESS_TOAST)).to.be(true); + + await navigateToEditAgentlessIntegrationPage(); + await PageObjects.header.waitUntilLoadingHasFinished(); + }; + + const showSuccessfulToast = async (testSubjectId: string) => { + return await testSubjects.exists(testSubjectId); + }; + + const getFirstCspmIntegrationPageIntegration = async () => { + const integration = await testSubjects.find(TEST_IDS.INTEGRATION_NAME_LINK); + return await integration.getVisibleText(); + }; + + const getFirstCspmIntegrationPageAgentlessIntegration = async () => { + const integration = await testSubjects.find(TEST_IDS.AGENTLESS_INTEGRATION_NAME_LINK); + return await integration.getVisibleText(); + }; + + const getFirstCspmIntegrationPageAgent = async () => { + const agent = await testSubjects.find(TEST_IDS.AGENT_POLICY_NAME_LINK); + // this is assuming that the agent was just created therefor should be the first element + return await agent.getVisibleText(); + }; + + const getFirstCspmIntegrationPageAgentlessStatus = async () => { + const agent = await testSubjects.find(TEST_IDS.AGENTLESS_STATUS_BADGE); + // this is assuming that the agent was just created therefor should be the first element + return await agent.getVisibleText(); + }; + + const getAgentBasedPolicyValue = async () => { + const agentName = await testSubjects.find(TEST_IDS.CREATE_AGENT_POLICY_NAME_FIELD); + return await agentName.getAttribute('value'); + }; + + const closeAllOpenTabs = async () => { + const handles = await browser.getAllWindowHandles(); + logger.debug(`Found ${handles.length} tabs to clean up`); + try { + // Keep the first tab and close all others in reverse order + for (let i = handles.length - 1; i > 0; i--) { + await browser.switchTab(i); + await browser.closeCurrentWindow(); + logger.debug(`Closed tab ${i}`); + } + + // Switch back to the first tab + await browser.switchTab(0); + logger.debug('Successfully closed all extra tabs and returned to main tab'); + } catch (err) { + logger.error(`Error while closing tabs: ${err}`); + // Attempt to return to first tab even if there was an error + try { + await browser.switchTab(0); + } catch (switchErr) { + logger.error(`Error switching back to first tab: ${switchErr}`); + } + } + }; + + return { + cisAzure, + cisAws, + cisGcp, + navigateToAddIntegrationWithVersionPage, + navigateToAddIntegrationCspmPage, + navigateToAddIntegrationCspmWithVersionPage, + navigateToAddIntegrationCnvmPage, + navigateToAddIntegrationKspmPage, + navigateToIntegrationCspList, + getUrlOnPostInstallModal, + isRadioButtonChecked, + clickPolicyToBeEdited, + clickFirstElementOnIntegrationTable, + clickFirstElementOnIntegrationTableAddAgent, + clickLaunchAndGetCurrentUrl, + getIntegrationFormEntirePage, + getIntegrationPolicyTable, + getIntegrationFormEditPage, + findOptionInPage, + clickOptionButton, + selectAwsCredentials, + selectSetupTechnology, + getSetupTechnologyRadio, + clickSaveButton, + clickSaveIntegrationButton, + clickAccordianButton, + getPostInstallModal, + fillInTextField, + chooseDropDown, + getFieldValueInEditPage, + doesStringExistInCodeBlock, + getFieldValueInAddAgentFlyout, + selectValue, + getValueInEditPage, + isOptionChecked, + checkIntegrationPliAuthBlockExists, + getReplaceSecretButton, + getSecretComponentReplaceButton, + inputUniqueIntegrationName, + getFieldAttributeValue, + getElementText, + createAgentlessIntegration, + editAgentlessIntegration, + testSubjectIds, + inputIntegrationName, + getFirstCspmIntegrationPageIntegration, + getFirstCspmIntegrationPageAgentlessIntegration, + getFirstCspmIntegrationPageAgent, + getFirstCspmIntegrationPageAgentlessStatus, + getAgentBasedPolicyValue, + showSuccessfulToast, + showSetupTechnologyComponent, + navigateToEditIntegrationPage, + navigateToEditAgentlessIntegrationPage, + closeAllOpenTabs, + waitUntilLaunchCloudFormationButtonAppears, + showCredentialJsonSecretPanel, + isSaveButtonEnabled, + clickAwsPolicyOption, + clickGcpPolicyOption, + clickAzurePolicyOption, + fillInComboBox, + }; +} diff --git a/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/alerts_page.ts b/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/alerts_page.ts new file mode 100644 index 0000000000000..1ff3c12330b03 --- /dev/null +++ b/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/alerts_page.ts @@ -0,0 +1,121 @@ +/* + * 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 from '@kbn/expect'; +import { FtrService } from '@kbn/test-suites-xpack/functional/ftr_provider_context'; +import { testSubjectIds } from './constants/test_subject_ids'; + +const { + ALERT_TABLE_ROW_CSS_SELECTOR, + EVENT_PREVIEW_SECTION_TEST_ID, + VISUALIZATIONS_SECTION_HEADER_TEST_ID, + VISUALIZATIONS_SECTION_CONTENT_TEST_ID, + GRAPH_PREVIEW_CONTENT_TEST_ID, + GRAPH_PREVIEW_LOADING_TEST_ID, +} = testSubjectIds; + +export class AlertsPageObject extends FtrService { + private readonly retry = this.ctx.getService('retry'); + private readonly pageObjects = this.ctx.getPageObjects(['common', 'header']); + private readonly testSubjects = this.ctx.getService('testSubjects'); + private readonly defaultTimeoutMs = this.ctx.getService('config').get('timeouts.waitFor'); + + async navigateToAlertsPage(urlQueryParams: string = ''): Promise { + await this.pageObjects.common.navigateToUrlWithBrowserHistory( + 'securitySolution', + '/alerts', + `${urlQueryParams && `?${urlQueryParams}`}`, + { + ensureCurrentUrl: false, + } + ); + await this.pageObjects.header.waitUntilLoadingHasFinished(); + } + + getAbsoluteTimerangeFilter(from: string, to: string) { + return `timerange=(global:(linkTo:!(),timerange:(from:%27${from}%27,kind:absolute,to:%27${to}%27)))`; + } + + getFlyoutFilter(alertId: string) { + return `flyout=(preview:!(),right:(id:document-details-right,params:(id:%27${alertId}%27,indexName:.internal.alerts-security.alerts-default-000001,scopeId:alerts-page)))`; + } + + /** + * Clicks the refresh button on the Alerts page and waits for it to complete + */ + async clickRefresh(): Promise { + await this.ensureOnAlertsPage(); + await this.testSubjects.click('querySubmitButton'); + + // wait for refresh to complete + await this.retry.waitFor( + 'Alerts pages refresh button to be enabled', + async (): Promise => { + const refreshButton = await this.testSubjects.find('querySubmitButton'); + + return (await refreshButton.isDisplayed()) && (await refreshButton.isEnabled()); + } + ); + } + + async ensureOnAlertsPage(): Promise { + await this.testSubjects.existOrFail('detectionsAlertsPage'); + } + + async waitForListToHaveAlerts(timeoutMs?: number): Promise { + const allEventRows = await this.testSubjects.findService.allByCssSelector( + ALERT_TABLE_ROW_CSS_SELECTOR + ); + + if (!Boolean(allEventRows.length)) { + await this.retry.waitForWithTimeout( + 'waiting for alerts to show up on alerts page', + timeoutMs ?? this.defaultTimeoutMs, + async (): Promise => { + await this.clickRefresh(); + + const allEventRowsInner = await this.testSubjects.findService.allByCssSelector( + ALERT_TABLE_ROW_CSS_SELECTOR + ); + + return Boolean(allEventRowsInner.length); + } + ); + } + } + + flyout = { + expandVisualizations: async (): Promise => { + const contentEl = await this.testSubjects.find(VISUALIZATIONS_SECTION_CONTENT_TEST_ID); + const isVisualizationVisible = (await contentEl.getSize()).height > 0; + + if (!isVisualizationVisible) { + await this.testSubjects.click(VISUALIZATIONS_SECTION_HEADER_TEST_ID); + } + }, + + assertGraphPreviewVisible: async () => { + return await this.testSubjects.existOrFail(GRAPH_PREVIEW_CONTENT_TEST_ID); + }, + + assertGraphNodesNumber: async (expected: number) => { + await this.flyout.waitGraphIsLoaded(); + const graph = await this.testSubjects.find(GRAPH_PREVIEW_CONTENT_TEST_ID); + await graph.scrollIntoView(); + const nodes = await graph.findAllByCssSelector('.react-flow__nodes .react-flow__node'); + expect(nodes.length).to.be(expected); + }, + + waitGraphIsLoaded: async () => { + await this.testSubjects.missingOrFail(GRAPH_PREVIEW_LOADING_TEST_ID, { timeout: 10000 }); + }, + + assertEventPreviewPanelIsOpen: async () => { + await this.testSubjects.existOrFail(EVENT_PREVIEW_SECTION_TEST_ID, { timeout: 10000 }); + }, + }; +} diff --git a/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/benchmark_page.ts b/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/benchmark_page.ts new file mode 100644 index 0000000000000..03c234f8d3693 --- /dev/null +++ b/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/benchmark_page.ts @@ -0,0 +1,116 @@ +/* + * 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 from '@kbn/expect'; +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; +import { WebElementWrapper } from '@kbn/ftr-common-functional-ui-services'; +import type { FtrProviderContext } from '../../ftr_provider_context'; + +export const CSP_BECNHMARK_TABLE = 'csp_benchmarks_table'; + +export function BenchmarkPagePageProvider({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'header']); + const retry = getService('retry'); + const supertest = getService('supertest'); + const log = getService('log'); + + /** + * required before indexing findings + */ + const waitForPluginInitialized = (): Promise => + retry.try(async () => { + log.debug('Check CSP plugin is initialized'); + const response = await supertest + .get('/internal/cloud_security_posture/status?check=init') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .expect(200); + expect(response.body).to.eql({ isPluginInitialized: true }); + log.debug('CSP plugin is initialized'); + }); + + const benchmarkPage = { + doesBenchmarkTableExists: async () => { + return await testSubjects.find('csp_benchmarks_table'); + }, + + getBenchmarkTableRows: async () => { + const benchmarkTable = await testSubjects.find(CSP_BECNHMARK_TABLE); + const tableRows = await benchmarkTable.findAllByXpath(`//tbody//tr`); + return tableRows; + }, + + getCellData: async (row: WebElementWrapper, cellDataTestSubj: string) => { + const cell = await row.findByTestSubject(cellDataTestSubj); + return await cell.getVisibleText(); + }, + + getEvaluatedCellData: async (row: WebElementWrapper) => { + return await benchmarkPage.getCellData(row, 'benchmark-table-column-evaluated'); + }, + + getComplianceCellData: async (row: WebElementWrapper) => { + return await benchmarkPage.getCellData(row, 'benchmark-table-column-compliance'); + }, + + getCisNameCellData: async (row: WebElementWrapper) => { + return await benchmarkPage.getCellData(row, 'benchmark-table-column-cis-name'); + }, + + isEvaluationEmpty: async (row: WebElementWrapper) => { + try { + const notEvaluated = await row.findAllByTestSubject('benchmark-not-evaluated-account', 200); + return notEvaluated.length > 0; + } catch (error) { + if (error.name === 'StaleElementReferenceError' || error.name === 'NoSuchElementError') { + return false; + } + throw error; + } + }, + + isComplianceEmpty: async (row: WebElementWrapper) => { + try { + const noCompliance = await row.findAllByTestSubject('benchmark-score-no-findings', 200); + return noCompliance.length > 0; + } catch (error) { + if (error.name === 'StaleElementReferenceError' || error.name === 'NoSuchElementError') { + return false; + } + throw error; + } + }, + }; + + const navigateToBenchnmarkPage = async (space?: string) => { + const options = space + ? { + basePath: `/s/${space}`, + shouldUseHashForSubUrl: false, + } + : { + shouldUseHashForSubUrl: false, + }; + + await PageObjects.common.navigateToUrl( + 'securitySolution', // Defined in Security Solution plugin + `cloud_security_posture/benchmarks/`, + options + ); + await PageObjects.header.waitUntilLoadingHasFinished(); + }; + + return { + waitForPluginInitialized, + navigateToBenchnmarkPage, + benchmarkPage, + }; +} diff --git a/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/constants/test_subject_ids.ts b/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/constants/test_subject_ids.ts new file mode 100644 index 0000000000000..ad469a2fb7b0a --- /dev/null +++ b/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/constants/test_subject_ids.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const testSubjectIds = { + CIS_AWS_OPTION_TEST_ID: 'cisAwsTestId', + AWS_SINGLE_ACCOUNT_TEST_ID: 'awsSingleTestId', + AWS_MANUAL_TEST_ID: 'aws-manual-setup-option', + AWS_CREDENTIAL_SELECTOR: 'aws-credentials-type-selector', + ROLE_ARN_TEST_ID: 'awsRoleArnInput', + DIRECT_ACCESS_KEY_ID_TEST_ID: 'awsDirectAccessKeyId', + DIRECT_ACCESS_SECRET_KEY_TEST_ID: 'passwordInput-secret-access-key', + TEMP_ACCESS_KEY_ID_TEST_ID: 'awsTemporaryKeysAccessKeyId', + TEMP_ACCESS_KEY_SECRET_KEY_TEST_ID: 'passwordInput-secret-access-key', + TEMP_ACCESS_SESSION_TOKEN_TEST_ID: 'awsTemporaryKeysSessionToken', + SHARED_CREDENTIALS_FILE_TEST_ID: 'awsSharedCredentialFile', + SHARED_CREDETIALS_PROFILE_NAME_TEST_ID: 'awsCredentialProfileName', + CIS_GCP_OPTION_TEST_ID: 'cisGcpTestId', + GCP_ORGANIZATION_TEST_ID: 'gcpOrganizationAccountTestId', + GCP_SINGLE_ACCOUNT_TEST_ID: 'gcpSingleAccountTestId', + GCP_CLOUD_SHELL_TEST_ID: 'gcpGoogleCloudShellOptionTestId', + GCP_MANUAL_TEST_ID: 'gcpManualOptionTestId', + PRJ_ID_TEST_ID: 'project_id_test_id', + ORG_ID_TEST_ID: 'organization_id_test_id', + CREDENTIALS_TYPE_TEST_ID: 'credentials_type_test_id', + CREDENTIALS_FILE_TEST_ID: 'credentials_file_test_id', + CREDENTIALS_JSON_TEST_ID: 'textAreaInput-credentials-json', + CIS_AZURE_OPTION_TEST_ID: 'cisAzureTestId', + CIS_AZURE_SINGLE_SUB_TEST_ID: 'azureSingleAccountTestId', + AZURE_CREDENTIAL_SELECTOR: 'azure-credentials-type-selector', + CIS_AZURE_INPUT_FIELDS_TEST_SUBJECTS: { + TENANT_ID: 'cisAzureTenantId', + CLIENT_ID: 'cisAzureClientId', + CLIENT_SECRET: 'passwordInput-client-secret', + CLIENT_CERTIFICATE_PATH: 'cisAzureClientCertificatePath', + CLIENT_CERTIFICATE_PASSWORD: 'passwordInput-client-certificate-password', + CLIENT_USERNAME: 'cisAzureClientUsername', + CLIENT_PASSWORD: 'cisAzureClientPassword', + }, + CIS_AZURE_SETUP_FORMAT_TEST_SUBJECTS: { + ARM_TEMPLATE: 'cisAzureArmTemplate', + MANUAL: 'cisAzureManual', + }, + EVENT_PREVIEW_SECTION_TEST_ID: 'previewSection', + EVENTS_TABLE_ROW_CSS_SELECTOR: '[data-test-subj="events-viewer-panel"] .euiDataGridRow', + VISUALIZATIONS_SECTION_HEADER_TEST_ID: 'securitySolutionFlyoutVisualizationsHeader', + VISUALIZATIONS_SECTION_CONTENT_TEST_ID: 'securitySolutionFlyoutVisualizationsContent', + GRAPH_PREVIEW_CONTENT_TEST_ID: 'securitySolutionFlyoutGraphPreviewContent', + GRAPH_PREVIEW_LOADING_TEST_ID: 'securitySolutionFlyoutGraphPreviewLoading', + GRAPH_PREVIEW_TITLE_LINK_TEST_ID: 'securitySolutionFlyoutGraphPreviewTitleLink', + NODE_EXPAND_BUTTON_TEST_ID: 'cloudSecurityGraphNodeExpandButton', + GRAPH_INVESTIGATION_TEST_ID: 'cloudSecurityGraphGraphInvestigation', + GRAPH_NODE_EXPAND_POPOVER_TEST_ID: 'cloudSecurityGraphGraphInvestigationGraphNodeExpandPopover', + GRAPH_NODE_POPOVER_EXPLORE_RELATED_TEST_ID: + 'cloudSecurityGraphGraphInvestigationExploreRelatedEntities', + GRAPH_NODE_POPOVER_SHOW_ACTIONS_BY_TEST_ID: + 'cloudSecurityGraphGraphInvestigationShowActionsByEntity', + GRAPH_NODE_POPOVER_SHOW_ACTIONS_ON_TEST_ID: + 'cloudSecurityGraphGraphInvestigationShowActionsOnEntity', + GRAPH_NODE_POPOVER_SHOW_ENTITY_DETAILS_ITEM_ID: + 'cloudSecurityGraphGraphInvestigationShowEntityDetails', + GRAPH_LABEL_EXPAND_POPOVER_TEST_ID: 'cloudSecurityGraphGraphInvestigationGraphLabelExpandPopover', + GRAPH_LABEL_EXPAND_POPOVER_SHOW_EVENTS_WITH_THIS_ACTION_ITEM_ID: + 'cloudSecurityGraphGraphInvestigationShowEventsWithThisAction', + GRAPH_LABEL_EXPAND_POPOVER_SHOW_EVENT_DETAILS_ITEM_ID: + 'cloudSecurityGraphGraphInvestigationShowEventDetails', + GRAPH_ACTIONS_TOGGLE_SEARCH_ID: 'cloudSecurityGraphGraphInvestigationToggleSearch', + GRAPH_ACTIONS_INVESTIGATE_IN_TIMELINE_ID: + 'cloudSecurityGraphGraphInvestigationInvestigateInTimeline', + ALERT_TABLE_ROW_CSS_SELECTOR: '[data-test-subj="alertsTableIsLoaded"] .euiDataGridRow', + SETUP_TECHNOLOGY_SELECTOR: 'setup-technology-selector', + DIRECT_ACCESS_KEYS: 'direct_access_keys', + SETUP_TECHNOLOGY_SELECTOR_AGENTLESS_RADIO: 'setup-technology-agentless-radio', + SETUP_TECHNOLOGY_SELECTOR_AGENT_BASED_RADIO: 'setup-technology-agent-based-radio', + NAMESPACE_INPUT: 'namespaceInput', + ADVANCED_OPTION_ACCORDION: 'advancedOptionsAccordion', +}; diff --git a/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/csp_dashboard_page.ts b/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/csp_dashboard_page.ts new file mode 100644 index 0000000000000..5e1e9885c7a3d --- /dev/null +++ b/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/csp_dashboard_page.ts @@ -0,0 +1,223 @@ +/* + * 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 from '@kbn/expect'; +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; +import type { FtrProviderContext } from '../../ftr_provider_context'; + +// Defined in CSP plugin +const LATEST_FINDINGS_INDEX = 'security_solution-cloud_security_posture.misconfiguration_latest'; + +export function CspDashboardPageProvider({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'header']); + const retry = getService('retry'); + const es = getService('es'); + const supertest = getService('supertest'); + const log = getService('log'); + + /** + * required before indexing findings + */ + const waitForPluginInitialized = (): Promise => + retry.try(async () => { + log.debug('Check CSP plugin is initialized'); + const response = await supertest + .get('/internal/cloud_security_posture/status?check=init') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .expect(200); + expect(response.body).to.eql({ isPluginInitialized: true }); + log.debug('CSP plugin is initialized'); + }); + + const index = { + remove: () => es.indices.delete({ index: LATEST_FINDINGS_INDEX, ignore_unavailable: true }), + add: async (findingsMock: T[]) => { + await Promise.all( + findingsMock.map((finding) => + es.index({ + index: LATEST_FINDINGS_INDEX, + document: finding, + }) + ) + ); + }, + }; + + const TAB_TYPES = { + CLOUD: 'Cloud', + KUBERNETES: 'Kubernetes', + } as const; + + const dashboard = { + getDashboardPageHeader: () => testSubjects.find('cloud-posture-dashboard-page-header'), + + getDashboardTabs: async () => { + await PageObjects.header.waitUntilLoadingHasFinished(); + const dashboardPageHeader = await dashboard.getDashboardPageHeader(); + return await dashboardPageHeader.findByClassName('euiTabs'); + }, + + getCloudTab: async () => { + const tabs = await dashboard.getDashboardTabs(); + return await tabs.findByXpath(`//span[text()="${TAB_TYPES.CLOUD}"]`); + }, + + getKubernetesTab: async () => { + const tabs = await dashboard.getDashboardTabs(); + return await tabs.findByXpath(`//span[text()="${TAB_TYPES.KUBERNETES}"]`); + }, + + clickTab: async (tab: (typeof TAB_TYPES)[keyof typeof TAB_TYPES]) => { + if (tab === TAB_TYPES.CLOUD) { + const cloudTab = await dashboard.getCloudTab(); + await cloudTab.click(); + } + if (tab === TAB_TYPES.KUBERNETES) { + const k8sTab = await dashboard.getKubernetesTab(); + await k8sTab.click(); + } + }, + + getAllComplianceScoresByCisSection: async (tab: (typeof TAB_TYPES)[keyof typeof TAB_TYPES]) => { + await dashboard.getDashoard(tab); + const pageContainer = await testSubjects.find('pageContainer'); + return await pageContainer.findAllByTestSubject('cloudSecurityFindingsComplianceScore'); + }, + + getDashoard: async (tab: (typeof TAB_TYPES)[keyof typeof TAB_TYPES]) => { + if (tab === TAB_TYPES.CLOUD) { + return await dashboard.getCloudDashboard(); + } + if (tab === TAB_TYPES.KUBERNETES) { + return await dashboard.getKubernetesDashboard(); + } + }, + + getFindingsLinks: async (tab: (typeof TAB_TYPES)[keyof typeof TAB_TYPES]) => { + await dashboard.getDashoard(tab); + const pageContainer = await testSubjects.find('pageContainer'); + return [ + await pageContainer.findByTestSubject('dashboard-summary-passed-findings'), + await pageContainer.findByTestSubject('dashboard-summary-failed-findings'), + ...(await pageContainer.findAllByTestSubject('grouped-findings-evaluation-link')), + ...(await pageContainer.findAllByTestSubject('view-all-failed-findings')), + ...(await pageContainer.findAllByTestSubject('benchmark-section-bench-name')), + ...(await pageContainer.findAllByTestSubject('benchmark-asset-type')), + ...(await pageContainer.findAllByTestSubject('compliance-score-section-passed')), + ...(await pageContainer.findAllByTestSubject('compliance-score-section-failed')), + ]; + }, + + getFindingsLinkAtIndex: async ( + tab: (typeof TAB_TYPES)[keyof typeof TAB_TYPES], + linkIndex = 0 + ) => { + const allLinks = await dashboard.getFindingsLinks(tab); + return allLinks[linkIndex]; + }, + + getFindingsLinksCount: async (tab: (typeof TAB_TYPES)[keyof typeof TAB_TYPES]) => { + const allLinks = await dashboard.getFindingsLinks(tab); + return allLinks.length; + }, + + getIntegrationDashboardContainer: () => testSubjects.find('dashboard-container'), + + // Cloud Dashboard + + getCloudDashboard: async () => { + await dashboard.clickTab(TAB_TYPES.CLOUD); + return await testSubjects.find('cloud-dashboard-container'); + }, + + getCloudSummarySection: async () => { + await dashboard.getCloudDashboard(); + return await testSubjects.find('dashboard-summary-section'); + }, + + getAllCloudComplianceScores: async () => { + await dashboard.getCloudDashboard(); + return await testSubjects.findAll('dashboard-summary-section-compliance-score'); + }, + + getCloudComplianceScore: async () => { + await dashboard.getCloudSummarySection(); + return await testSubjects.find('dashboard-summary-section-compliance-score'); + }, + + getCloudResourcesEvaluatedCard: async () => { + await dashboard.getCloudDashboard(); + return await testSubjects.find('dashboard-counter-card-resources-evaluated'); + }, + + getCloudResourcesEvaluated: async () => { + const resourcesEvaluatedCard = await dashboard.getCloudResourcesEvaluatedCard(); + return await resourcesEvaluatedCard.findByXpath('//div/p/span'); + }, + + // Kubernetes Dashboard + + getKubernetesDashboard: async () => { + await dashboard.clickTab(TAB_TYPES.KUBERNETES); + return await testSubjects.find('kubernetes-dashboard-container'); + }, + + getKubernetesSummarySection: async () => { + await dashboard.getKubernetesDashboard(); + return await testSubjects.find('dashboard-summary-section'); + }, + + getKubernetesComplianceScore: async () => { + await retry.waitFor( + 'Cloud posture dashboard summary section to be displayed', + async () => !!(await dashboard.getKubernetesSummarySection()) + ); + + return await testSubjects.find('dashboard-summary-section-compliance-score'); + }, + + getKubernetesResourcesEvaluatedCard: async () => { + await dashboard.getKubernetesDashboard(); + return await testSubjects.find('dashboard-counter-card-resources-evaluated'); + }, + + getKubernetesResourcesEvaluated: async () => { + const resourcesEvaluatedCard = await dashboard.getKubernetesResourcesEvaluatedCard(); + return await resourcesEvaluatedCard.findByXpath('//div/p/span'); + }, + }; + + const navigateToComplianceDashboardPage = async (space?: string) => { + const options = space + ? { + basePath: `/s/${space}`, + shouldUseHashForSubUrl: false, + } + : { + shouldUseHashForSubUrl: false, + }; + + await PageObjects.common.navigateToUrl( + 'securitySolution', // Defined in Security Solution plugin + 'cloud_security_posture/dashboard', + options + ); + }; + + return { + waitForPluginInitialized, + navigateToComplianceDashboardPage, + dashboard, + index, + TAB_TYPES, + }; +} diff --git a/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/expanded_flyout_graph.ts b/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/expanded_flyout_graph.ts new file mode 100644 index 0000000000000..3f8b20e61ea37 --- /dev/null +++ b/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/expanded_flyout_graph.ts @@ -0,0 +1,183 @@ +/* + * 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 from '@kbn/expect'; +import { GenericFtrService } from '@kbn/test'; +import type { WebElementWrapper } from '@kbn/ftr-common-functional-ui-services'; +import type { FilterBarService } from '@kbn/test-suites-src/functional/services/filter_bar'; +import type { QueryBarProvider } from './services/query_bar_provider'; +import type { FtrProviderContext } from '../../ftr_provider_context'; +import { testSubjectIds } from './constants/test_subject_ids'; + +const { + GRAPH_PREVIEW_TITLE_LINK_TEST_ID, + NODE_EXPAND_BUTTON_TEST_ID, + GRAPH_INVESTIGATION_TEST_ID, + GRAPH_NODE_EXPAND_POPOVER_TEST_ID, + GRAPH_NODE_POPOVER_EXPLORE_RELATED_TEST_ID, + GRAPH_NODE_POPOVER_SHOW_ACTIONS_BY_TEST_ID, + GRAPH_NODE_POPOVER_SHOW_ACTIONS_ON_TEST_ID, + GRAPH_NODE_POPOVER_SHOW_ENTITY_DETAILS_ITEM_ID, + GRAPH_LABEL_EXPAND_POPOVER_TEST_ID, + GRAPH_LABEL_EXPAND_POPOVER_SHOW_EVENTS_WITH_THIS_ACTION_ITEM_ID, + GRAPH_LABEL_EXPAND_POPOVER_SHOW_EVENT_DETAILS_ITEM_ID, + GRAPH_ACTIONS_TOGGLE_SEARCH_ID, + GRAPH_ACTIONS_INVESTIGATE_IN_TIMELINE_ID, +} = testSubjectIds; + +type Filter = Parameters[0]; + +export class ExpandedFlyoutGraph extends GenericFtrService { + private readonly pageObjects = this.ctx.getPageObjects(['common', 'header']); + private readonly testSubjects = this.ctx.getService('testSubjects'); + private readonly filterBar = this.ctx.getService('filterBar'); + + async expandGraph(): Promise { + await this.testSubjects.click(GRAPH_PREVIEW_TITLE_LINK_TEST_ID); + } + + async waitGraphIsLoaded(): Promise { + await this.testSubjects.existOrFail(GRAPH_INVESTIGATION_TEST_ID, { timeout: 10000 }); + } + + async assertGraphNodesNumber(expected: number): Promise { + await this.waitGraphIsLoaded(); + const graph = await this.testSubjects.find(GRAPH_INVESTIGATION_TEST_ID); + await graph.scrollIntoView(); + const nodes = await graph.findAllByCssSelector('.react-flow__nodes .react-flow__node'); + expect(nodes.length).to.be(expected); + } + + async toggleSearchBar(): Promise { + await this.testSubjects.click(GRAPH_ACTIONS_TOGGLE_SEARCH_ID); + } + + async selectNode(nodeId: string): Promise { + await this.waitGraphIsLoaded(); + const graph = await this.testSubjects.find(GRAPH_INVESTIGATION_TEST_ID); + await graph.scrollIntoView(); + const nodes = await graph.findAllByCssSelector( + `.react-flow__nodes .react-flow__node[data-id="${nodeId}"]` + ); + expect(nodes.length).to.be(1); + await nodes[0].moveMouseTo(); + return nodes[0]; + } + + async clickOnNodeExpandButton( + nodeId: string, + popoverId: string = GRAPH_NODE_EXPAND_POPOVER_TEST_ID + ): Promise { + const node = await this.selectNode(nodeId); + const expandButton = await node.findByTestSubject(NODE_EXPAND_BUTTON_TEST_ID); + await expandButton.click(); + await this.testSubjects.existOrFail(popoverId); + } + + async showActionsByEntity(nodeId: string): Promise { + await this.clickOnNodeExpandButton(nodeId); + await this.testSubjects.click(GRAPH_NODE_POPOVER_SHOW_ACTIONS_BY_TEST_ID); + await this.pageObjects.header.waitUntilLoadingHasFinished(); + } + + async showActionsOnEntity(nodeId: string): Promise { + await this.clickOnNodeExpandButton(nodeId); + await this.testSubjects.click(GRAPH_NODE_POPOVER_SHOW_ACTIONS_ON_TEST_ID); + await this.pageObjects.header.waitUntilLoadingHasFinished(); + } + + async showEntityDetails(nodeId: string): Promise { + await this.clickOnNodeExpandButton(nodeId); + await this.testSubjects.click(GRAPH_NODE_POPOVER_SHOW_ENTITY_DETAILS_ITEM_ID); + await this.pageObjects.header.waitUntilLoadingHasFinished(); + } + + async hideActionsOnEntity(nodeId: string): Promise { + await this.clickOnNodeExpandButton(nodeId); + const btnText = await this.testSubjects.getVisibleText( + GRAPH_NODE_POPOVER_SHOW_ACTIONS_ON_TEST_ID + ); + expect(btnText).to.be('Hide actions done to this entity'); + await this.testSubjects.click(GRAPH_NODE_POPOVER_SHOW_ACTIONS_ON_TEST_ID); + await this.pageObjects.header.waitUntilLoadingHasFinished(); + } + + async exploreRelatedEntities(nodeId: string): Promise { + await this.clickOnNodeExpandButton(nodeId); + await this.testSubjects.click(GRAPH_NODE_POPOVER_EXPLORE_RELATED_TEST_ID); + await this.pageObjects.header.waitUntilLoadingHasFinished(); + } + + async showEventsOfSameAction(nodeId: string): Promise { + await this.clickOnNodeExpandButton(nodeId, GRAPH_LABEL_EXPAND_POPOVER_TEST_ID); + await this.testSubjects.click(GRAPH_LABEL_EXPAND_POPOVER_SHOW_EVENTS_WITH_THIS_ACTION_ITEM_ID); + await this.pageObjects.header.waitUntilLoadingHasFinished(); + } + + async showEventOrAlertDetails(nodeId: string): Promise { + await this.clickOnNodeExpandButton(nodeId, GRAPH_LABEL_EXPAND_POPOVER_TEST_ID); + await this.testSubjects.click(GRAPH_LABEL_EXPAND_POPOVER_SHOW_EVENT_DETAILS_ITEM_ID); + await this.pageObjects.header.waitUntilLoadingHasFinished(); + } + + async hideEventsOfSameAction(nodeId: string): Promise { + await this.clickOnNodeExpandButton(nodeId, GRAPH_LABEL_EXPAND_POPOVER_TEST_ID); + const btnText = await this.testSubjects.getVisibleText( + GRAPH_LABEL_EXPAND_POPOVER_SHOW_EVENTS_WITH_THIS_ACTION_ITEM_ID + ); + expect(btnText).to.be('Hide related events'); + await this.testSubjects.click(GRAPH_LABEL_EXPAND_POPOVER_SHOW_EVENTS_WITH_THIS_ACTION_ITEM_ID); + await this.pageObjects.header.waitUntilLoadingHasFinished(); + } + + async expectFilterTextEquals(filterIdx: number, expected: string): Promise { + const filters = await this.filterBar.getFiltersLabel(); + expect(filters.length).to.be.greaterThan(filterIdx); + expect(filters[filterIdx]).to.be(expected); + } + + async expectFilterPreviewEquals(filterIdx: number, expected: string): Promise { + await this.clickEditFilter(filterIdx); + + const filterPreview = await this.filterBar.getFilterEditorPreview(); + expect(filterPreview).to.be(expected); + + await this.filterBar.ensureFieldEditorModalIsClosed(); + } + + async clickEditFilter(filterIdx: number): Promise { + await this.filterBar.clickEditFilterById(filterIdx.toString()); + } + + async clearAllFilters(): Promise { + await this.testSubjects.click(`${GRAPH_INVESTIGATION_TEST_ID} > showQueryBarMenu`); + await this.testSubjects.click('filter-sets-removeAllFilters'); + await this.pageObjects.header.waitUntilLoadingHasFinished(); + } + + async addFilter(filter: Filter): Promise { + await this.testSubjects.click(`${GRAPH_INVESTIGATION_TEST_ID} > addFilter`); + await this.filterBar.createFilter(filter); + await this.testSubjects.scrollIntoView('saveFilter'); + await this.testSubjects.clickWhenNotDisabled('saveFilter'); + await this.pageObjects.header.waitUntilLoadingHasFinished(); + } + + async clickOnInvestigateInTimelineButton(): Promise { + await this.testSubjects.click(GRAPH_ACTIONS_INVESTIGATE_IN_TIMELINE_ID); + await this.pageObjects.header.waitUntilLoadingHasFinished(); + } + + async setKqlQuery(kql: string): Promise { + const queryBarProvider: QueryBarProvider = this.ctx.getService('queryBarProvider'); + + const queryBar = queryBarProvider.getQueryBar(GRAPH_INVESTIGATION_TEST_ID); + await queryBar.setQuery(kql); + await queryBar.submitQuery(); + await this.pageObjects.header.waitUntilLoadingHasFinished(); + } +} diff --git a/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/findings_page.ts b/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/findings_page.ts new file mode 100644 index 0000000000000..50ea2d4672063 --- /dev/null +++ b/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/findings_page.ts @@ -0,0 +1,399 @@ +/* + * 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 from '@kbn/expect'; +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import type { FtrProviderContext } from '../../ftr_provider_context'; + +// Defined in CSP plugin +const FINDINGS_INDEX = 'logs-cloud_security_posture.findings-default'; +const FINDINGS_LATEST_INDEX = 'logs-cloud_security_posture.findings_latest-default'; +export const VULNERABILITIES_INDEX_DEFAULT_NS = + 'logs-cloud_security_posture.vulnerabilities-default'; +export const CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN = + 'logs-cloud_security_posture.vulnerabilities_latest-default'; + +export function FindingsPageProvider({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'header']); + const retry = getService('retry'); + const es = getService('es'); + const supertest = getService('supertest'); + const log = getService('log'); + + /** + * required before indexing findings + */ + const waitForPluginInitialized = (): Promise => + retry.try(async () => { + log.debug('Check CSP plugin is initialized'); + const response = await supertest + .get('/internal/cloud_security_posture/status?check=init') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .expect(200); + expect(response.body).to.eql({ isPluginInitialized: true }); + log.debug('CSP plugin is initialized'); + }); + + const deleteByQuery = async (index: string) => { + await es.deleteByQuery({ + index, + query: { + match_all: {}, + }, + ignore_unavailable: true, + refresh: true, + }); + }; + + const insertOperation = (index: string, findingsMock: Array>) => { + return findingsMock.flatMap((doc) => [{ index: { _index: index } }, doc]); + }; + + const index = { + remove: () => + Promise.all([deleteByQuery(FINDINGS_INDEX), deleteByQuery(FINDINGS_LATEST_INDEX)]), + add: async (findingsMock: Array>) => { + await es.bulk({ + refresh: true, + operations: [ + ...insertOperation(FINDINGS_INDEX, findingsMock), + ...insertOperation(FINDINGS_LATEST_INDEX, findingsMock), + ], + }); + }, + }; + + const vulnerabilitiesIndex = { + remove: () => + Promise.all([ + deleteByQuery(VULNERABILITIES_INDEX_DEFAULT_NS), + deleteByQuery(CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN), + ]), + add: async (findingsMock: Array>) => { + await es.bulk({ + refresh: true, + operations: [ + ...insertOperation(VULNERABILITIES_INDEX_DEFAULT_NS, findingsMock), + ...insertOperation(CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN, findingsMock), + ], + }); + }, + }; + + const detectionRuleApi = { + remove: async () => { + await supertest + .post('/api/detection_engine/rules/_bulk_action?dry_run=false') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') + .send({ + action: 'delete', + query: '', + }) + .expect(200); + }, + }; + + const distributionBar = { + filterBy: async (type: 'passed' | 'failed') => + testSubjects.click(type === 'failed' ? 'distribution_bar_failed' : 'distribution_bar_passed'), + }; + + const createNotInstalledObject = (notInstalledSubject: string) => ({ + getElement() { + return testSubjects.find(notInstalledSubject, 15 * 1000); + }, + + async navigateToAction(actionTestSubject: string) { + return await retry.try(async () => { + await testSubjects.click(actionTestSubject); + await PageObjects.header.waitUntilLoadingHasFinished(); + + const result = await testSubjects.exists('createPackagePolicy_pageTitle'); + + if (!result) { + throw new Error('Integration installation page not found'); + } + }); + }, + }); + + const createDataTableObject = (tableTestSubject: string) => ({ + getElement() { + return testSubjects.find(tableTestSubject); + }, + + async getHeaders() { + const element = await this.getElement(); + return await element.findAllByCssSelector('.euiDataGridHeader'); + }, + + async getColumnIndex(columnName: string) { + const element = await this.getElement(); + const columnIndexAttr = await ( + await element.findByCssSelector(`[data-gridcell-column-id="${columnName}"]`) + ).getAttribute('data-gridcell-column-index'); + expect(columnIndexAttr).to.not.be(null); + const columnIndex = parseInt(columnIndexAttr ?? '-1', 10); + expect(columnIndex).to.be.greaterThan(-1); + return columnIndex; + }, + + async getColumnHeaderCell(columnName: string) { + const headers = await this.getHeaders(); + const headerIndexes = await Promise.all(headers.map((header) => header.getVisibleText())); + const columnIndex = headerIndexes.findIndex((i) => i === columnName); + return headers[columnIndex]; + }, + + async getRowsCount() { + const element = await this.getElement(); + const rows = await element.findAllByCssSelector('.euiDataGridRow'); + return rows.length; + }, + + async getFindingsCount(type: 'passed' | 'failed') { + const element = await this.getElement(); + const items = await element.findAllByCssSelector(`span[data-test-subj="${type}_finding"]`); + return items.length; + }, + + async getRowIndexForValue(columnName: string, value: string) { + const values = await this.getColumnValues(columnName); + const rowIndex = values.indexOf(value); + expect(rowIndex).to.be.greaterThan(-1); + return rowIndex; + }, + + async getFilterElementButton(rowIndex: number, columnIndex: number | string, negated = false) { + const tableElement = await this.getElement(); + const button = negated ? 'filterOutButton' : 'filterForButton'; + const selector = `[data-gridcell-row-index="${rowIndex}"][data-gridcell-column-index="${columnIndex}"] button[data-test-subj="${button}"]`; + return tableElement.findByCssSelector(selector); + }, + + async addCellFilter(columnName: string, cellValue: string, negated = false) { + const columnIndex = await this.getColumnIndex(columnName); + const rowIndex = await this.getRowIndexForValue(columnName, cellValue); + const filterElement = await this.getFilterElementButton(rowIndex, columnIndex, negated); + await filterElement.click(); + }, + + async getColumnValues(columnName: string) { + const tableElement = await this.getElement(); + const selector = `.euiDataGridRowCell[data-gridcell-column-id="${columnName}"]`; + const columnCells = await tableElement.findAllByCssSelector(selector); + + return await Promise.all(columnCells.map((cell) => cell.getVisibleText())); + }, + + async hasColumnValue(columnName: string, value: string) { + const values = await this.getColumnValues(columnName); + return values.includes(value); + }, + + async toggleColumnSort(columnName: string, direction: 'asc' | 'desc') { + const currentSorting = await testSubjects.find('dataGridColumnSortingButton'); + const currentSortingText = await currentSorting.getVisibleText(); + await currentSorting.click(); + + if (currentSortingText !== 'Sort fields') { + const clearSortButton = await testSubjects.find('dataGridColumnSortingClearButton'); + await clearSortButton.click(); + } + + const selectSortFieldButton = await testSubjects.find('dataGridColumnSortingSelectionButton'); + await selectSortFieldButton.click(); + + const sortField = await testSubjects.find( + `dataGridColumnSortingPopoverColumnSelection-${columnName}` + ); + await sortField.click(); + + const sortDirection = await testSubjects.find( + `euiDataGridColumnSorting-sortColumn-${columnName}-${direction}` + ); + await sortDirection.click(); + await currentSorting.click(); + }, + + async openFlyoutAt(rowIndex: number) { + const table = await this.getElement(); + const flyoutButton = await table.findAllByTestSubject('docTableExpandToggleColumn'); + await flyoutButton[rowIndex].click(); + }, + + async toggleEditDataViewFieldsOption(columnId: string) { + const element = await this.getElement(); + const column = await element.findByCssSelector(`[data-gridcell-column-id="${columnId}"]`); + const button = await column.findByCssSelector('.euiDataGridHeaderCell__button'); + return await button.click(); + }, + }); + + const navigateToLatestFindingsPage = async (space?: string) => { + const options = space + ? { + basePath: `/s/${space}`, + shouldUseHashForSubUrl: false, + } + : { + shouldUseHashForSubUrl: false, + }; + + await PageObjects.common.navigateToUrl( + 'securitySolution', // Defined in Security Solution plugin + 'cloud_security_posture/findings/configurations', + options + ); + }; + + const navigateToLatestVulnerabilitiesPage = async (space?: string) => { + const options = space + ? { + basePath: `/s/${space}`, + shouldUseHashForSubUrl: false, + } + : { + shouldUseHashForSubUrl: false, + }; + await PageObjects.common.navigateToUrl( + 'securitySolution', // Defined in Security Solution plugin + 'cloud_security_posture/findings/vulnerabilities', + options + ); + }; + + const navigateToMisconfigurations = async (space?: string) => { + const options = space + ? { + basePath: `/s/${space}`, + shouldUseHashForSubUrl: false, + } + : { + shouldUseHashForSubUrl: false, + }; + + await PageObjects.common.navigateToUrl( + 'securitySolution', // Defined in Security Solution plugin + 'cloud_security_posture/findings/configurations', + options + ); + }; + + const latestFindingsTable = createDataTableObject('latest_findings_table'); + const latestVulnerabilitiesTable = createDataTableObject('latest_vulnerabilities_table'); + + const notInstalledVulnerabilities = createNotInstalledObject('cnvm-integration-not-installed'); + const notInstalledCSP = createNotInstalledObject('cloud_posture_page_package_not_installed'); + const thirdPartyIntegrationsNoVulnerabilitiesFindingsPrompt = createNotInstalledObject( + '3p-integrations-no-vulnerabilities-findings-prompt' + ); + const thirdPartyIntegrationsNoMisconfigurationsFindingsPrompt = createNotInstalledObject( + '3p-integrations-no-misconfigurations-findings-prompt' + ); + + const vulnerabilityDataGrid = { + getVulnerabilityTable: async () => testSubjects.find('euiDataGrid'), + }; + + const createFlyoutObject = (tableTestSubject: string) => ({ + async getElement() { + return await testSubjects.find(tableTestSubject); + }, + async clickTakeActionButton() { + const element = await this.getElement(); + const button = await element.findByCssSelector('[data-test-subj="csp:take_action"] button'); + await button.click(); + return button; + }, + async clickTakeActionCreateRuleButton() { + await this.clickTakeActionButton(); + const button = await testSubjects.find('csp:create_rule'); + await button.click(); + return button; + }, + async getVisibleText(testSubj: string) { + const element = await this.getElement(); + return await (await element.findByTestSubject(testSubj)).getVisibleText(); + }, + }); + + const misconfigurationsFlyout = createFlyoutObject('rightSection'); + + const groupSelector = (testSubj = 'group-selector-dropdown') => ({ + async getElement() { + return await testSubjects.find(testSubj); + }, + async setValue(value: string) { + const contextMenu = await testSubjects.find('groupByContextMenu'); + const menuItems = await contextMenu.findAllByCssSelector('button.euiContextMenuItem'); + const menuItemsOptions = await Promise.all(menuItems.map((item) => item.getVisibleText())); + const menuItemValueIndex = menuItemsOptions.findIndex((item) => item === value); + await menuItems[menuItemValueIndex].click(); + return await testSubjects.missingOrFail('is-loading-grouping-table', { timeout: 5000 }); + }, + async openDropDown() { + const element = await this.getElement(); + await element.click(); + }, + }); + + const findingsGrouping = async (testSubj = 'cloudSecurityGrouping') => ({ + async getElement() { + return await testSubjects.find(testSubj); + }, + async getGroupCount() { + const element = await this.getElement(); + const groupCount = await element.findByTestSubject('group-count'); + return await groupCount.getVisibleText(); + }, + async getUnitCount() { + const element = await this.getElement(); + const unitCount = await element.findByTestSubject('unit-count'); + return await unitCount.getVisibleText(); + }, + async getRowAtIndex(rowIndex: number) { + const element = await this.getElement(); + const row = await element.findAllByTestSubject('grouping-accordion'); + return await row[rowIndex]; + }, + }); + const isLatestFindingsTableThere = async () => { + const table = await testSubjects.findAll('docTable'); + return table.length > 0; + }; + + const getUnprivilegedPrompt = async () => { + return await testSubjects.find('status-api-unprivileged'); + }; + + return { + navigateToLatestFindingsPage, + navigateToLatestVulnerabilitiesPage, + navigateToMisconfigurations, + latestFindingsTable, + latestVulnerabilitiesTable, + notInstalledVulnerabilities, + notInstalledCSP, + thirdPartyIntegrationsNoMisconfigurationsFindingsPrompt, + thirdPartyIntegrationsNoVulnerabilitiesFindingsPrompt, + index, + vulnerabilitiesIndex, + waitForPluginInitialized, + distributionBar, + vulnerabilityDataGrid, + misconfigurationsFlyout, + detectionRuleApi, + groupSelector, + findingsGrouping, + createDataTableObject, + isLatestFindingsTableThere, + getUnprivilegedPrompt, + }; +} diff --git a/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/index.ts b/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/index.ts new file mode 100644 index 0000000000000..333008ccaeb0c --- /dev/null +++ b/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FindingsPageProvider } from './findings_page'; +import { CspDashboardPageProvider } from './csp_dashboard_page'; +import { AddCisIntegrationFormPageProvider } from './add_cis_integration_form_page'; +import { VulnerabilityDashboardPageProvider } from './vulnerability_dashboard_page_object'; +import { BenchmarkPagePageProvider } from './benchmark_page'; +import { CspSecurityCommonProvider } from './security_common'; +import { RulePagePageProvider } from './rule_page'; +import { AlertsPageObject } from './alerts_page'; +import { NetworkEventsPageObject } from './network_events_page'; +import { ExpandedFlyoutGraph } from './expanded_flyout_graph'; +import { TimelinePageObject } from './timeline_page'; + +export const cloudSecurityPosturePageObjects = { + alerts: AlertsPageObject, + networkEvents: NetworkEventsPageObject, + expandedFlyoutGraph: ExpandedFlyoutGraph, + timeline: TimelinePageObject, + findings: FindingsPageProvider, + cloudPostureDashboard: CspDashboardPageProvider, + cisAddIntegration: AddCisIntegrationFormPageProvider, + vulnerabilityDashboard: VulnerabilityDashboardPageProvider, + rule: RulePagePageProvider, + benchmark: BenchmarkPagePageProvider, + cspSecurity: CspSecurityCommonProvider, +}; diff --git a/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/network_events_page.ts b/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/network_events_page.ts new file mode 100644 index 0000000000000..f5e75be8dbad8 --- /dev/null +++ b/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/network_events_page.ts @@ -0,0 +1,121 @@ +/* + * 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 from '@kbn/expect'; +import { FtrService } from '@kbn/test-suites-xpack/functional/ftr_provider_context'; +import { testSubjectIds } from './constants/test_subject_ids'; + +const { + EVENT_PREVIEW_SECTION_TEST_ID, + EVENTS_TABLE_ROW_CSS_SELECTOR, + VISUALIZATIONS_SECTION_HEADER_TEST_ID, + VISUALIZATIONS_SECTION_CONTENT_TEST_ID, + GRAPH_PREVIEW_CONTENT_TEST_ID, + GRAPH_PREVIEW_LOADING_TEST_ID, +} = testSubjectIds; + +export class NetworkEventsPageObject extends FtrService { + private readonly retry = this.ctx.getService('retry'); + private readonly pageObjects = this.ctx.getPageObjects(['common', 'header']); + private readonly testSubjects = this.ctx.getService('testSubjects'); + private readonly defaultTimeoutMs = this.ctx.getService('config').get('timeouts.waitFor'); + + async navigateToNetworkEventsPage(urlQueryParams: string = ''): Promise { + await this.pageObjects.common.navigateToUrlWithBrowserHistory( + 'securitySolution', + '/network/events', + `${urlQueryParams && `?${urlQueryParams}`}`, + { + ensureCurrentUrl: false, + } + ); + await this.pageObjects.header.waitUntilLoadingHasFinished(); + } + + getAbsoluteTimerangeFilter(from: string, to: string) { + return `timerange=(global:(linkTo:!(),timerange:(from:%27${from}%27,kind:absolute,to:%27${to}%27)))`; + } + + getFlyoutFilter(eventId: string) { + return `flyout=(preview:!(),right:(id:document-details-right,params:(id:%27${eventId}%27,indexName:logs-gcp.audit-default,scopeId:network-page-events)))`; + } + + /** + * Clicks the refresh button on the network events page and waits for it to complete + */ + async clickRefresh(): Promise { + await this.ensureOnNetworkEventsPage(); + await this.testSubjects.click('querySubmitButton'); + + // wait for refresh to complete + await this.retry.waitFor( + 'Network events pages refresh button to be enabled', + async (): Promise => { + const refreshButton = await this.testSubjects.find('querySubmitButton'); + + return (await refreshButton.isDisplayed()) && (await refreshButton.isEnabled()); + } + ); + } + + async ensureOnNetworkEventsPage(): Promise { + await this.testSubjects.existOrFail('network-details-headline'); + } + + async waitForListToHaveEvents(timeoutMs?: number): Promise { + const allEventRows = await this.testSubjects.findService.allByCssSelector( + EVENTS_TABLE_ROW_CSS_SELECTOR + ); + + if (!Boolean(allEventRows.length)) { + await this.retry.waitForWithTimeout( + 'waiting for events to show up on network events page', + timeoutMs ?? this.defaultTimeoutMs, + async (): Promise => { + await this.clickRefresh(); + + const allEventRowsInner = await this.testSubjects.findService.allByCssSelector( + EVENTS_TABLE_ROW_CSS_SELECTOR + ); + + return Boolean(allEventRowsInner.length); + } + ); + } + } + + flyout = { + expandVisualizations: async (): Promise => { + const contentEl = await this.testSubjects.find(VISUALIZATIONS_SECTION_CONTENT_TEST_ID); + const isVisualizationVisible = (await contentEl.getSize()).height > 0; + + if (!isVisualizationVisible) { + await this.testSubjects.click(VISUALIZATIONS_SECTION_HEADER_TEST_ID); + } + }, + + assertGraphPreviewVisible: async () => { + return await this.testSubjects.existOrFail(GRAPH_PREVIEW_CONTENT_TEST_ID); + }, + + assertGraphNodesNumber: async (expected: number) => { + await this.flyout.waitGraphIsLoaded(); + const graph = await this.testSubjects.find(GRAPH_PREVIEW_CONTENT_TEST_ID); + await graph.scrollIntoView(); + const nodes = await graph.findAllByCssSelector('.react-flow__nodes .react-flow__node'); + expect(nodes.length).to.be(expected); + }, + + waitGraphIsLoaded: async () => { + await this.testSubjects.missingOrFail(GRAPH_PREVIEW_LOADING_TEST_ID, { timeout: 10000 }); + }, + + assertEventPreviewPanelIsOpen: async () => { + await this.testSubjects.existOrFail(EVENT_PREVIEW_SECTION_TEST_ID, { timeout: 10000 }); + }, + }; +} diff --git a/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/rule_page.ts b/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/rule_page.ts new file mode 100644 index 0000000000000..1f9891d7f866e --- /dev/null +++ b/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/rule_page.ts @@ -0,0 +1,253 @@ +/* + * 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 from '@kbn/expect'; +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; +import type { FtrProviderContext } from '../../ftr_provider_context'; + +export const RULES_BULK_ACTION_BUTTON = 'bulk-action-button'; +export const RULES_BULK_ACTION_OPTION_ENABLE = 'bulk-action-option-enable'; +export const RULES_BULK_ACTION_OPTION_DISABLE = 'bulk-action-option-disable'; +export const RULES_SELECT_ALL_RULES = 'select-all-rules-button'; +export const RULES_CLEAR_ALL_RULES_SELECTION = 'clear-rules-selection-button'; +export const RULES_ROWS_ENABLE_SWITCH_BUTTON = 'rules-row-enable-switch-button'; +export const RULES_DISABLED_FILTER = 'rules-disabled-filter'; +export const RULES_ENABLED_FILTER = 'rules-enabled-filter'; +export const CIS_SECTION_FILTER = 'options-filter-popover-button-cis-section-multi-select-filter'; +export const RULE_NUMBER_FILTER = 'options-filter-popover-button-rule-number-multi-select-filter'; +export const RULE_NUMBER_FILTER_SEARCH_FIELD = 'rule-number-search-input'; +export const RULES_FLYOUT_SWITCH_BUTTON = 'rule-flyout-switch-button'; +export const TAKE_ACTION_BUTTON = 'csp:take_action'; +export const CLOSE_FLYOUT_BUTTON = 'euiFlyoutCloseButton'; +export const CLOSE_TOAST_BUTTON = 'toastCloseButton'; + +export function RulePagePageProvider({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'header']); + const retry = getService('retry'); + const supertest = getService('supertest'); + const log = getService('log'); + + /** + * required before indexing findings + */ + const waitForPluginInitialized = (): Promise => + retry.try(async () => { + log.debug('Check CSP plugin is initialized'); + const response = await supertest + .get('/internal/cloud_security_posture/status?check=init') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .expect(200); + expect(response.body).to.eql({ isPluginInitialized: true }); + log.debug('CSP plugin is initialized'); + }); + + const rulePage = { + toggleBulkActionButton: async () => { + const bulkActionButtonToBeClicked = await testSubjects.find(RULES_BULK_ACTION_BUTTON); + await retry.waitFor('bulk action options to be displayed', async () => { + await bulkActionButtonToBeClicked.click(); + const bulkActionOptions = await testSubjects.findAll(RULES_BULK_ACTION_OPTION_DISABLE); + return bulkActionOptions.length > 0; + }); + }, + + clickBulkActionOption: async (optionTestId: string) => { + const bulkActionOption = await testSubjects.find(optionTestId); + await bulkActionOption.click(); + }, + + isBulkActionOptionDisabled: async (optionTestId: string) => { + const bulkActionOption = await testSubjects.find(optionTestId); + return await bulkActionOption.getAttribute('disabled'); + }, + + clickSelectAllRules: async () => { + const selectAllRulesButton = await testSubjects.find(RULES_SELECT_ALL_RULES); + await selectAllRulesButton.click(); + }, + + clickClearAllRulesSelection: async () => { + const clearAllRulesSelectionButton = await testSubjects.find(RULES_CLEAR_ALL_RULES_SELECTION); + await clearAllRulesSelectionButton.click(); + }, + + clickEnableRulesRowSwitchButton: async (index: number) => { + const enableRulesRowSwitch = await testSubjects.findAll(RULES_ROWS_ENABLE_SWITCH_BUTTON); + await enableRulesRowSwitch[index].click(); + }, + + clickFilterButton: async (filterType: 'enabled' | 'disabled') => { + const filterButtonToClick = + (await filterType) === 'enabled' + ? await testSubjects.find(RULES_ENABLED_FILTER) + : await testSubjects.find(RULES_DISABLED_FILTER); + await filterButtonToClick.click(); + }, + + getEnableRulesRowSwitchButton: async () => { + const enableRulesRowSwitch = await testSubjects.findAll(RULES_ROWS_ENABLE_SWITCH_BUTTON); + return await enableRulesRowSwitch.length; + }, + + clickFilterPopover: async (filterType: 'section' | 'ruleNumber') => { + const filterPopoverButton = + (await filterType) === 'section' + ? await testSubjects.find(CIS_SECTION_FILTER) + : await testSubjects.find(RULE_NUMBER_FILTER); + + await filterPopoverButton.click(); + await PageObjects.header.waitUntilLoadingHasFinished(); + }, + + clickFilterPopOverOption: async (value: string) => { + const chosenValue = await testSubjects.find('options-filter-popover-item-' + value); + await chosenValue.click(); + }, + + filterTextInput: async (selector: string, value: string) => { + const textField = await testSubjects.find(selector); + await textField.type(value); + await PageObjects.header.waitUntilLoadingHasFinished(); + }, + + clickRulesNames: async (index: number) => { + const rulesNames = await testSubjects.findAll('csp_rules_table_row_item_name'); + await rulesNames[index].click(); + }, + + clickFlyoutEnableSwitchButton: async () => { + const rulesFlyoutEnableSwitchButton = await testSubjects.find(RULES_FLYOUT_SWITCH_BUTTON); + await rulesFlyoutEnableSwitchButton.click(); + }, + + getEnableSwitchButtonState: async () => { + const rulesFlyoutEnableSwitchButton = await testSubjects.find(RULES_FLYOUT_SWITCH_BUTTON); + return await rulesFlyoutEnableSwitchButton.getAttribute('aria-checked'); + }, + closeToasts: async () => { + const toasts = await testSubjects.findAll(CLOSE_TOAST_BUTTON); + for (const toast of toasts) { + await toast.click(); + } + }, + togglEnableRulesRowSwitchButton: async (index: number, filterType: 'enable' | 'disable') => { + const enableRulesRowSwitches = await testSubjects.findAll(RULES_ROWS_ENABLE_SWITCH_BUTTON); + const checked = await enableRulesRowSwitches[index].getAttribute('aria-checked'); + + if (filterType === 'enable' && checked) { + return; + } + if (filterType === 'disable' && !checked) { + return; + } + await enableRulesRowSwitches[index].click(); + }, + + clickCloseFlyoutButton: async () => { + const closeFlyoutButton = await testSubjects.find(CLOSE_FLYOUT_BUTTON); + await closeFlyoutButton.click(); + }, + + clickTakeActionButton: async () => { + const takeActionButton = await testSubjects.find(TAKE_ACTION_BUTTON); + await takeActionButton.click(); + }, + + clickTakeActionButtonOption: async (action: 'enable' | 'disable') => { + const takeActionOption = await testSubjects.find( + action + '-benchmark-rule-take-action-button' + ); + await takeActionOption.click(); + }, + + getCountersEmptyState: async () => { + return await testSubjects.exists('rules-counters-empty-state'); + }, + + getPostureScoreCounter: async () => { + return await testSubjects.find('rules-counters-posture-score-counter'); + }, + + clickPostureScoreButton: async () => { + const postureScoreButton = await testSubjects.find('rules-counters-posture-score-button'); + await postureScoreButton.click(); + }, + + getIntegrationsEvaluatedCounter: async () => { + return await testSubjects.find('rules-counters-integrations-evaluated-counter'); + }, + + clickIntegrationsEvaluatedButton: async () => { + await retry.try(async () => { + const integrationsEvaluatedButton = await testSubjects.find( + 'rules-counters-integrations-evaluated-button' + ); + // Check that href exists and is not empty + const href = await integrationsEvaluatedButton.getAttribute('href'); + if (!href) { + throw new Error('Integration link is not ready yet - href is empty'); + } + await integrationsEvaluatedButton.click(); + }); + }, + + getFailedFindingsCounter: async () => { + return await testSubjects.find('rules-counters-failed-findings-counter'); + }, + + clickFailedFindingsButton: async () => { + const failedFindingsButton = await testSubjects.find('rules-counters-failed-findings-button'); + await failedFindingsButton.click(); + }, + + getDisabledRulesCounter: async () => { + return await testSubjects.find('rules-counters-disabled-rules-counter'); + }, + + clickDisabledRulesButton: async () => { + const disabledRulesButton = await testSubjects.find('rules-counters-disabled-rules-button'); + await disabledRulesButton.click(); + }, + + doesElementExist: async (selector: string) => { + return await testSubjects.exists(selector); + }, + }; + + const navigateToRulePage = async ( + benchmarkCisId: string, + benchmarkCisVersion: string, + space?: string + ) => { + const options = space + ? { + basePath: `/s/${space}`, + shouldUseHashForSubUrl: false, + } + : { + shouldUseHashForSubUrl: false, + }; + + await PageObjects.common.navigateToUrl( + 'securitySolution', // Defined in Security Solution plugin + `cloud_security_posture/benchmarks/${benchmarkCisId}/${benchmarkCisVersion}/rules`, + options + ); + await PageObjects.header.waitUntilLoadingHasFinished(); + }; + + return { + waitForPluginInitialized, + navigateToRulePage, + rulePage, + }; +} diff --git a/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/security_common.ts b/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/security_common.ts new file mode 100644 index 0000000000000..639dc4befbc3a --- /dev/null +++ b/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/security_common.ts @@ -0,0 +1,133 @@ +/* + * 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 type { FtrProviderContext } from '../../ftr_provider_context'; + +export function CspSecurityCommonProvider({ getPageObjects, getService }: FtrProviderContext) { + const security = getService('security'); + const pageObjects = getPageObjects(['security']); + + const roles = [ + { + name: 'csp_viewer', + elasticsearch: { + indices: [ + { + names: ['logs-cloud_security_posture.findings-*'], + privileges: ['read'], + }, + { + names: ['logs-cloud_security_posture.findings_latest-*'], + privileges: ['read'], + }, + { + names: ['security_solution-cloud_security_posture.misconfiguration_latest'], + privileges: ['read'], + }, + + { + names: ['logs-cloud_security_posture.scores-*'], + privileges: ['read'], + }, + ], + }, + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], + }, + { + name: 'missing_access_findings_latest_role', + elasticsearch: { + indices: [ + { + names: ['logs-cloud_security_posture.findings-*'], + privileges: ['read'], + }, + { + names: ['logs-cloud_security_posture.scores-*'], + privileges: ['read'], + }, + ], + }, + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], + }, + ]; + + const users = [ + { + name: 'csp_read_user', + full_name: 'csp viewer', + password: 'test123', + roles: ['csp_viewer'], + }, + { + name: 'csp_missing_latest_findings_access_user', + full_name: 'missing latest findings index access', + password: 'csp123', + roles: ['missing_access_findings_latest_role'], + }, + ]; + + return { + async createRoles() { + for (const role of roles) { + await security.role.create(role.name, { + elasticsearch: role.elasticsearch, + kibana: role.kibana, + }); + } + }, + + async createUsers() { + for (const user of users) { + await security.user.create(user.name, { + password: user.password, + roles: user.roles, + full_name: user.full_name, + }); + } + }, + + async login(user: string) { + await pageObjects.security.login(user, this.getPasswordForUser(user), { + expectSpaceSelector: false, + }); + }, + + async logout() { + await pageObjects.security.forceLogout(); + }, + + async cleanRoles() { + for (const role of roles) { + await security.role.delete(role.name); + } + }, + + async cleanUsers() { + for (const user of users) { + await security.user.delete(user.name); + } + }, + + getPasswordForUser(user: string): string { + const userConfig = users.find((u) => u.name === user); + if (userConfig === undefined) { + throw new Error(`Can't log in user ${user} - not defined`); + } + return userConfig.password; + }, + }; +} diff --git a/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/services/query_bar_provider.ts b/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/services/query_bar_provider.ts new file mode 100644 index 0000000000000..e44db77ae6699 --- /dev/null +++ b/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/services/query_bar_provider.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrService } from '@kbn/test-suites-xpack/functional/ftr_provider_context'; + +export class QueryBarProvider extends FtrService { + private readonly testSubjects = this.ctx.getService('testSubjects'); + private readonly retry = this.ctx.getService('retry'); + private readonly log = this.ctx.getService('log'); + private readonly common = this.ctx.getPageObject('common'); + private readonly header = this.ctx.getPageObject('header'); + private readonly find = this.ctx.getService('find'); + + public getQueryBar(parentSelector?: string) { + const QUERY_INPUT_SELECTOR = parentSelector ? `${parentSelector} > queryInput` : 'queryInput'; + + const getQueryString = async (): Promise => { + return (await this.testSubjects.getAttribute(QUERY_INPUT_SELECTOR, 'value')) ?? ''; + }; + + const setQuery = async (query: string): Promise => { + this.log.debug(`QueryBar.setQuery(${query})`); + // Extra caution used because of flaky test here: https://github.com/elastic/kibana/issues/16978 doesn't seem + // to be actually setting the query in the query input based off + await this.retry.try(async () => { + await this.testSubjects.click(QUERY_INPUT_SELECTOR); + + // this.testSubjects.setValue uses input.clearValue which wasn't working, but input.clearValueWithKeyboard does. + // So the following lines do the same thing as input.setValue but with input.clearValueWithKeyboard instead. + const input = await this.find.activeElement(); + await input.clearValueWithKeyboard(); + await input.type(query); + const currentQuery = await getQueryString(); + if (currentQuery !== query) { + throw new Error( + `Failed to set query input to ${query}, instead query is ${currentQuery}` + ); + } + }); + }; + + const clearQuery = async (): Promise => { + await setQuery(''); + await this.common.pressTabKey(); // move outside of input into language switcher + await this.common.pressTabKey(); // move outside of language switcher so time picker appears + }; + + const submitQuery = async (): Promise => { + this.log.debug('QueryBar.submitQuery'); + await this.testSubjects.click(QUERY_INPUT_SELECTOR); + await this.common.pressEnterKey(); + await this.header.waitUntilLoadingHasFinished(); + }; + + return { + setQuery, + clearQuery, + submitQuery, + }; + } +} diff --git a/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/timeline_page.ts b/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/timeline_page.ts new file mode 100644 index 0000000000000..6cfb724e601c0 --- /dev/null +++ b/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/timeline_page.ts @@ -0,0 +1,103 @@ +/* + * 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 { subj as testSubjSelector } from '@kbn/test-subj-selector'; +import { FtrService } from '@kbn/test-suites-xpack/functional/ftr_provider_context'; + +const TIMELINE_CLOSE_BUTTON_TEST_SUBJ = 'timeline-modal-header-close-button'; +const TIMELINE_MODAL_PAGE_TEST_SUBJ = 'timeline'; +const TIMELINE_TAB_QUERY_TEST_SUBJ = 'timeline-tab-content-query'; + +const TIMELINE_CSS_SELECTOR = Object.freeze({ + /** The refresh button on the timeline view (top of view, next to the date selector) */ + refreshButton: `${testSubjSelector(TIMELINE_TAB_QUERY_TEST_SUBJ)} ${testSubjSelector( + 'superDatePickerApplyTimeButton' + )} `, +}); + +export class TimelinePageObject extends FtrService { + private readonly pageObjects = this.ctx.getPageObjects(['common', 'header']); + private readonly testSubjects = this.ctx.getService('testSubjects'); + private readonly retry = this.ctx.getService('retry'); + private readonly defaultTimeoutMs = this.ctx.getService('config').get('timeouts.waitFor'); + private readonly logger = this.ctx.getService('log'); + + async closeTimeline(): Promise { + if (await this.testSubjects.exists(TIMELINE_CLOSE_BUTTON_TEST_SUBJ)) { + await this.testSubjects.click(TIMELINE_CLOSE_BUTTON_TEST_SUBJ); + await this.testSubjects.waitForHidden(TIMELINE_MODAL_PAGE_TEST_SUBJ); + } + } + + async ensureTimelineIsOpen(): Promise { + await this.testSubjects.existOrFail(TIMELINE_MODAL_PAGE_TEST_SUBJ); + } + + /** + * From a visible timeline, clicks the "view details" for an event on the list + * @param index + */ + async showEventDetails(index: number = 0): Promise { + await this.ensureTimelineIsOpen(); + await this.testSubjects.findService.clickByCssSelector( + `${testSubjSelector('event')}:nth-child(${index + 1}) ${testSubjSelector('expand-event')}` + ); + await this.testSubjects.existOrFail('eventDetails'); + } + + /** + * Clicks the Refresh button at the top of the timeline page and waits for the refresh to complete + */ + async clickRefresh(): Promise { + await this.ensureTimelineIsOpen(); + await this.pageObjects.header.waitUntilLoadingHasFinished(); + await ( + await this.testSubjects.findService.byCssSelector(TIMELINE_CSS_SELECTOR.refreshButton) + ).isEnabled(); + await this.testSubjects.findService.clickByCssSelector(TIMELINE_CSS_SELECTOR.refreshButton); + await this.retry.waitFor( + 'Timeline refresh button to be enabled', + async (): Promise => { + return ( + await this.testSubjects.findService.byCssSelector(TIMELINE_CSS_SELECTOR.refreshButton) + ).isEnabled(); + } + ); + } + + /** + * Check to see if the timeline has events in the list + */ + async hasEvents(): Promise { + const eventRows = await this.testSubjects.findService.allByCssSelector( + `${testSubjSelector(TIMELINE_MODAL_PAGE_TEST_SUBJ)} [role="row"]` + ); + + return eventRows.length > 0; + } + + /** + * Waits for events to be displayed in the timeline. It will click on the "Refresh" button to trigger a data fetch + * @param timeoutMs + */ + async waitForEvents(timeoutMs?: number): Promise { + if (await this.hasEvents()) { + this.logger.info(`Timeline already has events displayed`); + return; + } + + await this.retry.waitForWithTimeout( + 'waiting for events to show up on timeline', + timeoutMs ?? this.defaultTimeoutMs, + async (): Promise => { + await this.clickRefresh(); + + return this.hasEvents(); + } + ); + } +} diff --git a/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/vulnerability_dashboard_page_object.ts b/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/vulnerability_dashboard_page_object.ts new file mode 100644 index 0000000000000..ca32c88f5016a --- /dev/null +++ b/x-pack/test_serverless/functional/page_objects/cloud_security_posture_page_objects/vulnerability_dashboard_page_object.ts @@ -0,0 +1,129 @@ +/* + * 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 from '@kbn/expect'; +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import type { FtrProviderContext } from '../../ftr_provider_context'; + +const VULNERABILITIES_LATEST_INDEX = 'logs-cloud_security_posture.vulnerabilities_latest-default'; +const BENCHMARK_SCORES_INDEX = 'logs-cloud_security_posture.scores-default'; + +export function VulnerabilityDashboardPageProvider({ + getService, + getPageObjects, +}: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'header']); + const retry = getService('retry'); + const es = getService('es'); + const supertest = getService('supertest'); + const log = getService('log'); + const testSubjects = getService('testSubjects'); + + /** + * required before indexing findings + */ + const waitForPluginInitialized = (): Promise => + retry.try(async () => { + log.debug('Check CSP plugin is initialized'); + const response = await supertest + .get('/internal/cloud_security_posture/status?check=init') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .expect(200); + expect(response.body).to.eql({ isPluginInitialized: true }); + log.debug('CSP plugin is initialized'); + }); + + const navigateToVulnerabilityDashboardPage = async (space?: string) => { + const options = space + ? { + basePath: `/s/${space}`, + shouldUseHashForSubUrl: false, + } + : { + shouldUseHashForSubUrl: false, + }; + + await PageObjects.common.navigateToUrl( + 'securitySolution', // Defined in Security Solution plugin + 'cloud_security_posture/vulnerability_dashboard', + options + ); + }; + + const index = { + addFindings: async (vulnerabilitiesMock: T[]) => { + await Promise.all( + vulnerabilitiesMock.map((vulnerabilityDoc) => + es.index({ + index: VULNERABILITIES_LATEST_INDEX, + document: vulnerabilityDoc, + refresh: true, + }) + ) + ); + }, + + addScores: async (scoresMock: T[]) => { + await Promise.all( + scoresMock.map((scoreDoc) => + es.index({ + index: BENCHMARK_SCORES_INDEX, + document: scoreDoc, + refresh: true, + }) + ) + ); + }, + + removeFindings: async () => { + const indexExists = await es.indices.exists({ index: VULNERABILITIES_LATEST_INDEX }); + + if (indexExists) { + await es.deleteByQuery({ + index: VULNERABILITIES_LATEST_INDEX, + query: { match_all: {} }, + refresh: true, + }); + } + }, + + removeScores: async () => { + const indexExists = await es.indices.exists({ index: BENCHMARK_SCORES_INDEX }); + + if (indexExists) { + await es.deleteByQuery({ + index: BENCHMARK_SCORES_INDEX, + query: { match_all: {} }, + refresh: true, + }); + } + }, + + deleteFindingsIndex: async () => { + const indexExists = await es.indices.exists({ index: VULNERABILITIES_LATEST_INDEX }); + + if (indexExists) { + await es.indices.delete({ index: VULNERABILITIES_LATEST_INDEX }); + } + }, + }; + + const dashboard = { + getDashboardPageHeader: () => testSubjects.find('vulnerability-dashboard-page-header'), + + getCriticalStat: () => testSubjects.find('critical-count-stat'), + getHighStat: () => testSubjects.find('high-count-stat'), + getMediumStat: () => testSubjects.find('medium-count-stat'), + }; + + return { + navigateToVulnerabilityDashboardPage, + waitForPluginInitialized, + index, + dashboard, + }; +} diff --git a/x-pack/test_serverless/functional/page_objects/index.ts b/x-pack/test_serverless/functional/page_objects/index.ts index f97f7c436595f..4cd319e74c305 100644 --- a/x-pack/test_serverless/functional/page_objects/index.ts +++ b/x-pack/test_serverless/functional/page_objects/index.ts @@ -7,7 +7,7 @@ import { pageObjects as xpackFunctionalPageObjects } from '@kbn/test-suites-xpack/functional/page_objects'; -import { cloudSecurityPosturePageObjects } from '@kbn/test-suites-xpack-security/cloud_security_posture_functional/page_objects'; +import { cloudSecurityPosturePageObjects } from './cloud_security_posture_page_objects'; import { SvlCommonPageProvider } from './svl_common_page'; import { SvlCommonNavigationProvider } from './svl_common_navigation'; import { SvlObltOnboardingPageProvider } from './svl_oblt_onboarding_page'; From 0be97dab2bfa9751e8aec1eb939a6616b6ea749c Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 2 Jul 2025 12:51:53 +0000 Subject: [PATCH 2/2] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- x-pack/test_serverless/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test_serverless/tsconfig.json b/x-pack/test_serverless/tsconfig.json index 18ce563f97982..77c414adc47cb 100644 --- a/x-pack/test_serverless/tsconfig.json +++ b/x-pack/test_serverless/tsconfig.json @@ -90,6 +90,7 @@ "@kbn/scout-info", "@kbn/streams-schema", "@kbn/test-suites-xpack-platform", - "@kbn/test-suites-xpack-security", + "@kbn/security-solution-plugin", + "@kbn/test-subj-selector", ] }