From 2de1f4a55805d4a6d4d3560e3025798e33db87b5 Mon Sep 17 00:00:00 2001 From: Rickyanto Ang Date: Sat, 19 Oct 2024 15:39:50 +0700 Subject: [PATCH] [Cloud Security] Cypress Test for Misconfiguration Preview and Table for Contextual Flyout (#193125) ## Summary This PR is for Cypress test for the Misconfiguration Preview and Data table --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .buildkite/pipelines/flaky_tests/groups.json | 8 + .../cloud_security_posture.yml | 30 +++ .../pipelines/pull_request/pipeline.ts | 16 ++ .../functional/cloud_security_posture.sh | 16 ++ .../cloud_security_posture_serverless.sh | 16 ++ .github/CODEOWNERS | 3 +- .../plugins/cloud_security_posture/README.md | 39 +++- ...isconfiguration_findings_details_table.tsx | 1 + .../misconfiguration_preview.tsx | 1 + .../cypress/README.md | 3 + .../misconfiguration_contextual_flyout.cy.ts | 219 ++++++++++++++++++ .../vulnerabilities_contextual_flyout.cy.ts | 24 +- .../security_solution_cypress/package.json | 5 +- 13 files changed, 365 insertions(+), 16 deletions(-) create mode 100644 .buildkite/pipelines/pull_request/security_solution/cloud_security_posture.yml create mode 100644 .buildkite/scripts/steps/functional/cloud_security_posture.sh create mode 100644 .buildkite/scripts/steps/functional/cloud_security_posture_serverless.sh create mode 100644 x-pack/test/security_solution_cypress/cypress/e2e/cloud_security_posture/misconfiguration_contextual_flyout.cy.ts rename x-pack/test/security_solution_cypress/cypress/e2e/{investigations/alerts/expandable_flyout => cloud_security_posture}/vulnerabilities_contextual_flyout.cy.ts (86%) diff --git a/.buildkite/pipelines/flaky_tests/groups.json b/.buildkite/pipelines/flaky_tests/groups.json index 9d47bdd850b94..77c3d23714d6f 100644 --- a/.buildkite/pipelines/flaky_tests/groups.json +++ b/.buildkite/pipelines/flaky_tests/groups.json @@ -91,6 +91,14 @@ { "key": "cypress/inventory_cypress", "name": "Inventory - Cypress" + }, + { + "key": "cypress/cloud_security_posture", + "name": "Cloud Security Posture - Cypress" + }, + { + "key": "cypress/cloud_security_posture_serverless", + "name": "[Serverless] Cloud Security Posture - Cypress" } ] } \ No newline at end of file diff --git a/.buildkite/pipelines/pull_request/security_solution/cloud_security_posture.yml b/.buildkite/pipelines/pull_request/security_solution/cloud_security_posture.yml new file mode 100644 index 0000000000000..7f5131b77f204 --- /dev/null +++ b/.buildkite/pipelines/pull_request/security_solution/cloud_security_posture.yml @@ -0,0 +1,30 @@ +steps: + - command: .buildkite/scripts/steps/functional/cloud_security_posture.sh + label: 'Cloud Security Posture Cypress Tests' + agents: + machineType: n2-standard-4 + preemptible: true + depends_on: + - build + - quick_checks + timeout_in_minutes: 60 + parallelism: 1 + retry: + automatic: + - exit_status: '-1' + limit: 1 + + - command: .buildkite/scripts/steps/functional/cloud_security_posture_serverless.sh + label: 'Cloud Security Posture Cypress Tests on Serverless' + agents: + machineType: n2-standard-4 + preemptible: true + depends_on: + - build + - quick_checks + timeout_in_minutes: 60 + parallelism: 1 + retry: + automatic: + - exit_status: '-1' + limit: 1 \ No newline at end of file diff --git a/.buildkite/scripts/pipelines/pull_request/pipeline.ts b/.buildkite/scripts/pipelines/pull_request/pipeline.ts index 8b9a4b62e6029..722c30cb42534 100644 --- a/.buildkite/scripts/pipelines/pull_request/pipeline.ts +++ b/.buildkite/scripts/pipelines/pull_request/pipeline.ts @@ -308,6 +308,22 @@ const getPipeline = (filename: string, removeSteps = true) => { ); } + if ( + (await doAnyChangesMatch([ + /^x-pack\/packages\/kbn-cloud-security-posture/, + /^x-pack\/plugins\/cloud_security_posture/, + /^x-pack\/plugins\/security_solution/, + /^x-pack\/test\/security_solution_cypress/, + ])) || + GITHUB_PR_LABELS.includes('ci:all-cypress-suites') + ) { + pipeline.push( + getPipeline( + '.buildkite/pipelines/pull_request/security_solution/cloud_security_posture.yml' + ) + ); + } + pipeline.push(getPipeline('.buildkite/pipelines/pull_request/post_build.yml')); // remove duplicated steps diff --git a/.buildkite/scripts/steps/functional/cloud_security_posture.sh b/.buildkite/scripts/steps/functional/cloud_security_posture.sh new file mode 100644 index 0000000000000..bea407e6a5b1c --- /dev/null +++ b/.buildkite/scripts/steps/functional/cloud_security_posture.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source .buildkite/scripts/steps/functional/common.sh + +export JOB=kibana-cloud-security-posture-cypress +export KIBANA_INSTALL_DIR=${KIBANA_BUILD_LOCATION} + +echo "--- Cloud Security Posture Workflows Cypress tests" + +cd x-pack/test/security_solution_cypress + +set +e + +yarn cypress:cloud_security_posture:run:ess; status=$?; yarn junit:merge || :; exit $status \ No newline at end of file diff --git a/.buildkite/scripts/steps/functional/cloud_security_posture_serverless.sh b/.buildkite/scripts/steps/functional/cloud_security_posture_serverless.sh new file mode 100644 index 0000000000000..57c9be111a151 --- /dev/null +++ b/.buildkite/scripts/steps/functional/cloud_security_posture_serverless.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source .buildkite/scripts/steps/functional/common.sh + +export JOB=kibana-cloud-security-posture-serverless-cypress +export KIBANA_INSTALL_DIR=${KIBANA_BUILD_LOCATION} + +echo "--- Cloud Security Posture Workflows Cypress tests on Serverless" + +cd x-pack/test/security_solution_cypress + +set +e + +yarn cypress:cloud_security_posture:run:serverless; status=$?; yarn junit:merge || :; exit $status diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a7a808f3d1d23..56fe95cd65b39 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1867,7 +1867,8 @@ x-pack/plugins/osquery @elastic/security-defend-workflows /x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.* @elastic/fleet @elastic/kibana-cloud-security-posture /x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/cloud_posture_third_party_support_callout.* @elastic/fleet @elastic/kibana-cloud-security-posture /x-pack/plugins/security_solution/public/cloud_security_posture @elastic/kibana-cloud-security-posture -/x-pack/test/security_solution_cypress/cypress/e2e/explore/hosts/vulnerabilities_contextual_flyout.cy.ts @elastic/kibana-cloud-security-posture +/x-pack/test/security_solution_cypress/cypress/e2e/cloud_security_posture/misconfiguration_contextual_flyout.cy.ts @elastic/kibana-cloud-security-posture +/x-pack/test/security_solution_cypress/cypress/e2e/cloud_security_posture/vulnerabilities_contextual_flyout.cy.ts @elastic/kibana-cloud-security-posture # Security Solution onboarding tour /x-pack/plugins/security_solution/public/common/components/guided_onboarding @elastic/security-threat-hunting-explore diff --git a/x-pack/plugins/cloud_security_posture/README.md b/x-pack/plugins/cloud_security_posture/README.md index bd0b7de6ac661..f608a614fca1c 100755 --- a/x-pack/plugins/cloud_security_posture/README.md +++ b/x-pack/plugins/cloud_security_posture/README.md @@ -20,6 +20,7 @@ For general guidelines, read [Kibana Testing Guide](https://www.elastic.co/guide 1. [End-to-End Tests](../../test/cloud_security_posture_functional/config.ts) 1. [Serverless API Integration tests](../../test_serverless/api_integration/test_suites/security/config.ts) 1. [Serverless End-to-End Tests](../../test_serverless/functional/test_suites/security/config.ts) +1. [Cypress End-to-End Tests](../../test/security_solution_cypress/cypress/e2e/cloud_security_posture) ### Tools @@ -73,6 +74,17 @@ yarn test:ftr --config x-pack/test_serverless/api_integration/test_suites/securi yarn test:ftr --config x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.ts ``` +Run [**End-to-End Cypress Tests**](https://github.com/elastic/kibana/tree/main/x-pack/test/security_solution_cypress/cypress): +> **Note** +> +> Run this from security_solution_cypress folder +```bash +yarn cypress:open:serverless +yarn cypress:open:ess +yarn cypress:cloud_security_posture:run:serverless +yarn cypress:cloud_security_posture:run:ess +``` + #### Run **FTR tests (integration or e2e) for development** Functional test runner (FTR) can be used separately with `ftr:runner` and `ftr:server`. This is convenient while developing tests. @@ -107,4 +119,29 @@ run serverless e2e tests: ```bash yarn test:ftr:server --config x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.ts yarn test:ftr:runner ---config x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.ts -``` \ No newline at end of file +``` + +#### Run **Cypress tests (e2e) for development** +When developing feature outside our plugin folder, instead of using FTRs for e2e test, we may use Cypress. Before running cypress, make sure you have installed it first. Like FTRs, we can run cypress in different environment, for example: + +run ess e2e tests: +```bash +yarn cypress:open:ess +``` + +run ess Cloud Security Posture e2e tests: +```bash +yarn cypress:cloud_security_posture:run:ess +``` + +run serverless e2e tests: +```bash +yarn cypress:open:serverless +``` + +run serverless Cloud Security Posture e2e tests: +```bash +yarn cypress:cloud_security_posture:run:serverless +``` + +Unlike FTR where we have to set server and runner separately, Cypress handles everything in 1 go, so just running the above the script is enough to get it running \ No newline at end of file diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/misconfiguration_findings_details_table.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/misconfiguration_findings_details_table.tsx index 81547f7bbf5ac..c03dc319952b5 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/misconfiguration_findings_details_table.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/misconfiguration_findings_details_table.tsx @@ -196,6 +196,7 @@ export const MisconfigurationFindingsDetailsTable = memo( columns={columns} pagination={pagination} onChange={onTableChange} + data-test-subj={'securitySolutionFlyoutMisconfigurationFindingsTable'} /> diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx index 0aae29395602e..433e493493e37 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx @@ -229,6 +229,7 @@ export const MisconfigurationsPreview = ({ css={css` font-weight: ${euiTheme.font.weight.semiBold}; `} + data-test-subj={'securitySolutionFlyoutInsightsMisconfigurationsTitleText'} > { + cy.get(CSP_INSIGHT_MISCONFIGURATION_TITLE).click(); +}; + +const timestamp = Date.now(); + +const date = new Date(timestamp); + +const iso8601String = date.toISOString(); + +const mockFindingHostName = (matches: boolean) => { + return { + '@timestamp': iso8601String, + host: { name: matches ? 'siem-kibana' : 'not-siem-kibana' }, + resource: { + id: '1234ABCD', + name: 'kubelet', + sub_type: 'lower case sub type', + }, + result: { evaluation: matches ? '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', + data_stream: { + dataset: 'cloud_security_posture.findings', + }, + }; +}; + +const mockFindingUserName = (matches: boolean) => { + return { + '@timestamp': iso8601String, + user: { name: matches ? 'test' : 'not-test' }, + resource: { + id: '1234ABCD', + name: 'kubelet', + sub_type: 'lower case sub type', + }, + result: { evaluation: matches ? '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', + data_stream: { + dataset: 'cloud_security_posture.findings', + }, + }; +}; + +const createMockFinding = (isNameMatches: boolean, findingType: 'host.name' | 'user.name') => { + return rootRequest({ + method: 'POST', + url: `${Cypress.env( + 'ELASTICSEARCH_URL' + )}/${CDR_LATEST_NATIVE_MISCONFIGURATIONS_INDEX_PATTERN}/_doc`, + body: + findingType === 'host.name' + ? mockFindingHostName(isNameMatches) + : mockFindingUserName(isNameMatches), + }); +}; + +const deleteDataStream = () => { + return rootRequest({ + method: 'DELETE', + url: `${Cypress.env( + 'ELASTICSEARCH_URL' + )}/_data_stream/${CDR_LATEST_NATIVE_MISCONFIGURATIONS_INDEX_PATTERN}`, + }); +}; + +describe('Alert Host details expandable flyout', { tags: ['@ess', '@serverless'] }, () => { + beforeEach(() => { + deleteAlertsAndRules(); + login(); + createRule(getNewRule()); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + }); + + context('Host name - Has misconfiguration findings', () => { + beforeEach(() => { + createMockFinding(true, 'host.name'); + cy.reload(); + expandFirstAlertHostFlyout(); + }); + + afterEach(() => { + /* Deleting data stream even though we don't create it because data stream is automatically created when Cloud security API is used */ + deleteDataStream(); + }); + + it('should display Misconfiguration preview under Insights Entities when it has Misconfiguration Findings', () => { + cy.log('check if Misconfiguration preview title shown'); + cy.get(CSP_INSIGHT_MISCONFIGURATION_TITLE).should('be.visible'); + }); + + it('should display insight tabs and findings table upon clicking on misconfiguration accordion', () => { + clickMisconfigurationTitle(); + cy.get(CSP_INSIGHT_TAB_TITLE).should('be.visible'); + cy.get(CSP_INSIGHT_TABLE).should('be.visible'); + }); + }); + + context( + 'Host name - Has misconfiguration findings but host name is not the same as alert host name', + () => { + beforeEach(() => { + createMockFinding(false, 'host.name'); + cy.reload(); + expandFirstAlertHostFlyout(); + }); + + afterEach(() => { + deleteDataStream(); + }); + + it('should display Misconfiguration preview under Insights Entities when it has Misconfiguration Findings', () => { + expandFirstAlertHostFlyout(); + + cy.log('check if Misconfiguration preview title is not shown'); + cy.get(CSP_INSIGHT_MISCONFIGURATION_TITLE).should('not.exist'); + }); + } + ); + + context('User name - Has misconfiguration findings', () => { + beforeEach(() => { + createMockFinding(true, 'user.name'); + cy.reload(); + expandFirstAlertUserFlyout(); + }); + + afterEach(() => { + deleteDataStream(); + }); + + it('should display Misconfiguration preview under Insights Entities when it has Misconfiguration Findings', () => { + cy.log('check if Misconfiguration preview title shown'); + cy.get(CSP_INSIGHT_MISCONFIGURATION_TITLE).should('be.visible'); + }); + + it('should display insight tabs and findings table upon clicking on misconfiguration accordion', () => { + clickMisconfigurationTitle(); + cy.get(CSP_INSIGHT_TAB_TITLE).should('be.visible'); + cy.get(CSP_INSIGHT_TABLE).should('be.visible'); + }); + }); + + context( + 'User name - Has misconfiguration findings but host name is not the same as alert host name', + () => { + beforeEach(() => { + createMockFinding(false, 'user.name'); + cy.reload(); + expandFirstAlertHostFlyout(); + }); + + afterEach(() => { + deleteDataStream(); + }); + + it('should display Misconfiguration preview under Insights Entities when it has Misconfiguration Findings', () => { + expandFirstAlertUserFlyout(); + + cy.log('check if Misconfiguration preview title is not shown'); + cy.get(CSP_INSIGHT_MISCONFIGURATION_TITLE).should('not.exist'); + }); + } + ); +}); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/vulnerabilities_contextual_flyout.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/cloud_security_posture/vulnerabilities_contextual_flyout.cy.ts similarity index 86% rename from x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/vulnerabilities_contextual_flyout.cy.ts rename to x-pack/test/security_solution_cypress/cypress/e2e/cloud_security_posture/vulnerabilities_contextual_flyout.cy.ts index fb83df1c79141..046864962318b 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/vulnerabilities_contextual_flyout.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/cloud_security_posture/vulnerabilities_contextual_flyout.cy.ts @@ -6,16 +6,16 @@ */ import { CDR_LATEST_NATIVE_VULNERABILITIES_INDEX_PATTERN } from '@kbn/cloud-security-posture-common'; -import { createRule } from '../../../../tasks/api_calls/rules'; -import { getNewRule } from '../../../../objects/rule'; -import { getDataTestSubjectSelector } from '../../../../helpers/common'; +import { createRule } from '../../tasks/api_calls/rules'; +import { getNewRule } from '../../objects/rule'; +import { getDataTestSubjectSelector } from '../../helpers/common'; -import { rootRequest, deleteAlertsAndRules } from '../../../../tasks/api_calls/common'; -import { expandFirstAlertHostFlyout } from '../../../../tasks/asset_criticality/common'; -import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule'; -import { login } from '../../../../tasks/login'; -import { ALERTS_URL } from '../../../../urls/navigation'; -import { visit } from '../../../../tasks/navigation'; +import { rootRequest, deleteAlertsAndRules } from '../../tasks/api_calls/common'; +import { expandFirstAlertHostFlyout } from '../../tasks/asset_criticality/common'; +import { waitForAlertsToPopulate } from '../../tasks/create_new_rule'; +import { login } from '../../tasks/login'; +import { ALERTS_URL } from '../../urls/navigation'; +import { visit } from '../../tasks/navigation'; const CSP_INSIGHT_VULNERABILITIES_TITLE = getDataTestSubjectSelector( 'securitySolutionFlyoutInsightsVulnerabilitiesTitleLink' @@ -27,10 +27,8 @@ const CSP_INSIGHT_VULNERABILITIES_TABLE = getDataTestSubjectSelector( const timestamp = Date.now(); -// Create a Date object using the timestamp const date = new Date(timestamp); -// Convert the Date object to ISO 8601 format const iso8601String = date.toISOString(); const getMockVulnerability = (isNameMatchesAlert: boolean) => { @@ -138,8 +136,7 @@ const deleteDataStream = () => { }); }; -// skipping because failure on MKI environment (https://buildkite.com/elastic/kibana-serverless-security-solution-quality-gate-investigations/builds/1390#01927579-caed-41bc-9440-3cf29629a263) -describe.skip('Alert Host details expandable flyout', { tags: ['@ess', '@serverless'] }, () => { +describe('Alert Host details expandable flyout', { tags: ['@ess', '@serverless'] }, () => { beforeEach(() => { deleteAlertsAndRules(); login(); @@ -167,6 +164,7 @@ describe.skip('Alert Host details expandable flyout', { tags: ['@ess', '@serverl }); afterEach(() => { + /* Deleting data stream even though we don't create it because data stream is automatically created when Cloud security API is used */ deleteDataStream(); }); diff --git a/x-pack/test/security_solution_cypress/package.json b/x-pack/test/security_solution_cypress/package.json index 5f7fc4fadf2bf..c7bc7fe1d94a0 100644 --- a/x-pack/test/security_solution_cypress/package.json +++ b/x-pack/test/security_solution_cypress/package.json @@ -19,6 +19,7 @@ "cypress:investigations:run:ess": "yarn cypress:ess --spec './cypress/e2e/investigations/**/*.cy.ts'", "cypress:explore:run:ess": "yarn cypress:ess --spec './cypress/e2e/explore/**/*.cy.ts'", "cypress:changed-specs-only:ess": "yarn cypress:ess --changed-specs-only --env burn=5", + "cypress:cloud_security_posture:run:ess": "yarn cypress:ess --spec './cypress/e2e/cloud_security_posture/**/*.cy.ts'", "cypress:burn:ess": "yarn cypress:ess --env burn=5", "junit:merge": "../../../node_modules/.bin/mochawesome-merge ../../../target/kibana-security-solution/cypress/results/mochawesome*.json > ../../../target/kibana-security-solution/cypress/results/output.json && ../../../node_modules/.bin/marge ../../../target/kibana-security-solution/cypress/results/output.json --reportDir ../../../target/kibana-security-solution/cypress/results && yarn junit:transform && mkdir -p ../../../target/junit && cp ../../../target/kibana-security-solution/cypress/results/*.xml ../../../target/junit/", "junit:transform": "node ../../plugins/security_solution/scripts/junit_transformer --pathPattern '../../../target/kibana-security-solution/cypress/results/*.xml' --rootDirectory ../../../ --reportName 'Security Solution Cypress' --writeInPlace", @@ -35,6 +36,7 @@ "cypress:investigations:run:serverless": "yarn cypress:serverless --spec './cypress/e2e/investigations/**/*.cy.ts'", "cypress:explore:run:serverless": "yarn cypress:serverless --spec './cypress/e2e/explore/**/*.cy.ts'", "cypress:changed-specs-only:serverless": "yarn cypress:serverless --changed-specs-only --env burn=5", + "cypress:cloud_security_posture:run:serverless": "yarn cypress:serverless --spec './cypress/e2e/cloud_security_posture/**/*.cy.ts'", "cypress:burn:serverless": "yarn cypress:serverless --env burn=2", "cypress:qa:serverless": "TZ=UTC NODE_OPTIONS=--openssl-legacy-provider node ../../plugins/security_solution/scripts/start_cypress_parallel_serverless --config-file ../../test/security_solution_cypress/cypress/cypress_ci_serverless_qa.config.ts", "cypress:open:qa:serverless": "yarn cypress:qa:serverless open", @@ -45,6 +47,7 @@ "cypress:run:qa:serverless:rule_management:prebuilt_rules": "yarn cypress:qa:serverless --spec './cypress/e2e/detection_response/rule_management/prebuilt_rules/**/*.cy.ts'", "cypress:run:qa:serverless:detection_engine": "yarn cypress:qa:serverless --spec './cypress/e2e/detection_response/detection_engine/!(exceptions)/**/*.cy.ts'", "cypress:run:qa:serverless:detection_engine:exceptions": "yarn cypress:qa:serverless --spec './cypress/e2e/detection_response/detection_engine/exceptions/**/*.cy.ts'", - "cypress:run:qa:serverless:ai_assistant": "yarn cypress:qa:serverless --spec './cypress/e2e/ai_assistant/**/*.cy.ts'" + "cypress:run:qa:serverless:ai_assistant": "yarn cypress:qa:serverless --spec './cypress/e2e/ai_assistant/**/*.cy.ts'", + "cypress:run:qa:serverless:cloud_security_posture": "yarn cypress:qa:serverless --spec './cypress/e2e/cloud_security_posture/**/*.cy.ts" } } \ No newline at end of file