diff --git a/.buildkite/scripts/lifecycle/post_command.sh b/.buildkite/scripts/lifecycle/post_command.sh index eeb7fa0ef410f..d0a5837ed5a67 100755 --- a/.buildkite/scripts/lifecycle/post_command.sh +++ b/.buildkite/scripts/lifecycle/post_command.sh @@ -15,6 +15,7 @@ if [[ "$IS_TEST_EXECUTION_STEP" == "true" ]]; then buildkite-agent artifact upload 'target/kibana-*' buildkite-agent artifact upload 'target/kibana-security-solution/**/*.png' buildkite-agent artifact upload 'target/kibana-osquery/**/*.png' + buildkite-agent artifact upload 'target/kibana-osquery/**/*.mp4' buildkite-agent artifact upload 'target/kibana-fleet/**/*.png' buildkite-agent artifact upload 'target/test-metrics/*' buildkite-agent artifact upload 'target/test-suites-ci-plan.json' diff --git a/x-pack/plugins/osquery/cypress/cypress.config.ts b/x-pack/plugins/osquery/cypress/cypress.config.ts index e3321d7ff6865..defc409a76719 100644 --- a/x-pack/plugins/osquery/cypress/cypress.config.ts +++ b/x-pack/plugins/osquery/cypress/cypress.config.ts @@ -11,6 +11,7 @@ import path from 'path'; import { safeLoad as loadYaml } from 'js-yaml'; import { readFileSync } from 'fs'; +import { getFailedSpecVideos } from './support/filter_videos'; import type { YamlRoleDefinitions } from '../../../test_serverless/shared/lib'; import { setupUserDataLoader } from '../../../test_serverless/functional/test_suites/security/cypress/support/setup_data_loader_tasks'; const ROLES_YAML_FILE_PATH = path.join( @@ -36,8 +37,9 @@ export default defineCypressConfig({ screenshotsFolder: '../../../target/kibana-osquery/cypress/screenshots', trashAssetsBeforeRuns: false, - video: false, + video: true, videosFolder: '../../../target/kibana-osquery/cypress/videos', + videoCompression: 15, viewportHeight: 900, viewportWidth: 1440, experimentalStudio: true, @@ -59,6 +61,7 @@ export default defineCypressConfig({ numTestsKeptInMemory: 3, setupNodeEvents(on, config) { setupUserDataLoader(on, config, { roleDefinitions, additionalRoleName: 'viewer' }); + on('after:spec', getFailedSpecVideos); return config; }, diff --git a/x-pack/plugins/osquery/cypress/e2e/all/add_integration.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/add_integration.cy.ts index 47691d1cdace6..b26e9b1787abe 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/add_integration.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/add_integration.cy.ts @@ -66,8 +66,7 @@ describe('ALL - Add Integration', { tags: ['@ess', '@serverless'] }, () => { } ); - // FLAKY: https://github.com/elastic/kibana/issues/169702 - describe.skip('Add and upgrade integration', { tags: ['@ess', '@serverless'] }, () => { + describe('Add and upgrade integration', { tags: ['@ess', '@serverless'] }, () => { const oldVersion = '0.7.4'; const [integrationName, policyName] = generateRandomStringName(2); let policyId: string; diff --git a/x-pack/plugins/osquery/cypress/e2e/all/alerts_automated_action_results.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/alerts_automated_action_results.cy.ts index 2aff49d395369..7b201bddd0ec6 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/alerts_automated_action_results.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/alerts_automated_action_results.cy.ts @@ -5,15 +5,19 @@ * 2.0. */ +import { initializeDataViews } from '../../tasks/login'; import { cleanupRule, loadRule } from '../../tasks/api_fixtures'; import { checkActionItemsInResults, loadRuleAlerts } from '../../tasks/live_query'; const UUID_REGEX = '[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}'; -// FLAKY: https://github.com/elastic/kibana/issues/169727 -describe.skip('Alert Flyout Automated Action Results', () => { +describe('Alert Flyout Automated Action Results', () => { let ruleId: string; + before(() => { + initializeDataViews(); + }); + beforeEach(() => { loadRule(true).then((data) => { ruleId = data.id; diff --git a/x-pack/plugins/osquery/cypress/e2e/all/alerts_cases.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/alerts_cases.cy.ts index 2d681bdc71729..3c93bef865b96 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/alerts_cases.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/alerts_cases.cy.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { initializeDataViews } from '../../tasks/login'; import { OSQUERY_FLYOUT_BODY_EDITOR } from '../../screens/live_query'; import { cleanupCase, @@ -29,6 +30,9 @@ describe('Alert Event Details - Cases', { tags: ['@ess', '@serverless'] }, () => let packId: string; let packName: string; const packData = packFixture(); + before(() => { + initializeDataViews(); + }); beforeEach(() => { loadPack(packData).then((data) => { diff --git a/x-pack/plugins/osquery/cypress/e2e/all/alerts_linked_apps.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/alerts_linked_apps.cy.ts index 8fca7b0164eef..945410e656beb 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/alerts_linked_apps.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/alerts_linked_apps.cy.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { initializeDataViews } from '../../tasks/login'; import { cleanupRule, loadRule } from '../../tasks/api_fixtures'; import { RESPONSE_ACTIONS_ITEM_0, RESPONSE_ACTIONS_ITEM_1 } from '../../tasks/response_actions'; import { @@ -25,7 +26,9 @@ describe( () => { let ruleId: string; let ruleName: string; - + before(() => { + initializeDataViews(); + }); beforeEach(() => { loadRule().then((data) => { ruleId = data.id; diff --git a/x-pack/plugins/osquery/cypress/e2e/all/alerts_multiple_agents.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/alerts_multiple_agents.cy.ts index 12d3f30ad632a..afea9bb8c0adf 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/alerts_multiple_agents.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/alerts_multiple_agents.cy.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { initializeDataViews } from '../../tasks/login'; import { cleanupRule, loadRule } from '../../tasks/api_fixtures'; import { inputQuery, @@ -14,8 +15,7 @@ import { } from '../../tasks/live_query'; import { OSQUERY_FLYOUT_BODY_EDITOR } from '../../screens/live_query'; -// FLAKY: https://github.com/elastic/kibana/issues/170157 -describe.skip( +describe( 'Alert Event Details - dynamic params', { tags: ['@ess', '@serverless'], @@ -25,6 +25,7 @@ describe.skip( let ruleName: string; before(() => { + initializeDataViews(); loadRule(true).then((data) => { ruleId = data.id; ruleName = data.name; diff --git a/x-pack/plugins/osquery/cypress/e2e/all/alerts_response_actions_form.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/alerts_response_actions_form.cy.ts index 78e7b8ff93aa5..15fb98540e438 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/alerts_response_actions_form.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/alerts_response_actions_form.cy.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { initializeDataViews } from '../../tasks/login'; import { cleanupPack, cleanupRule, @@ -31,7 +32,9 @@ describe('Alert Event Details - Response Actions Form', { tags: ['@ess', '@serve let packName: string; const packData = packFixture(); const multiQueryPackData = multiQueryPackFixture(); - + before(() => { + initializeDataViews(); + }); beforeEach(() => { loadPack(packData).then((data) => { packId = data.saved_object_id; @@ -55,6 +58,7 @@ describe('Alert Event Details - Response Actions Form', { tags: ['@ess', '@serve it('adds response actions with osquery with proper validation and form values', () => { cy.visit('/app/security/rules'); clickRuleName(ruleName); + cy.getBySel('globalLoadingIndicator').should('not.exist'); cy.getBySel('editRuleSettingsLink').click(); cy.getBySel('globalLoadingIndicator').should('not.exist'); closeDateTabIfVisible(); @@ -81,20 +85,22 @@ describe('Alert Event Details - Response Actions Form', { tags: ['@ess', '@serve cy.getBySel(OSQUERY_RESPONSE_ACTION_ADD_BUTTON).click(); - cy.getBySel(RESPONSE_ACTIONS_ITEM_2).within(() => { - cy.contains('Query is a required field'); - inputQuery('select * from uptime'); - cy.contains('Query is a required field').should('not.exist'); - cy.contains('Advanced').click(); - typeInECSFieldInput('{downArrow}{enter}'); - cy.getBySel('osqueryColumnValueSelect').type('days{downArrow}{enter}'); - cy.wait(1000); // wait for the validation to trigger - cypress is way faster than users ;) - }); + cy.getBySel(RESPONSE_ACTIONS_ITEM_2) + .within(() => { + cy.contains('Query is a required field'); + inputQuery('select * from uptime'); + cy.contains('Query is a required field').should('not.exist'); + cy.contains('Advanced').click(); + typeInECSFieldInput('{downArrow}{enter}'); + cy.getBySel('osqueryColumnValueSelect').type('days{downArrow}{enter}'); + }) + .clickOutside(); cy.getBySel('ruleEditSubmitButton').click(); cy.contains(`${ruleName} was saved`).should('exist'); closeToastIfVisible(); + cy.getBySel('globalLoadingIndicator').should('not.exist'); cy.getBySel('editRuleSettingsLink').click(); cy.getBySel('globalLoadingIndicator').should('not.exist'); cy.getBySel('edit-rule-actions-tab').click(); @@ -114,11 +120,13 @@ describe('Alert Event Details - Response Actions Form', { tags: ['@ess', '@serve cy.contains('select * from uptime1'); cy.getBySel('remove-response-action').click(); }); - cy.getBySel(RESPONSE_ACTIONS_ITEM_0).within(() => { - cy.contains('Search for a pack to run'); - cy.contains('Pack is a required field'); - cy.getBySel('comboBoxInput').type(`${packName}{downArrow}{enter}`); - }); + cy.getBySel(RESPONSE_ACTIONS_ITEM_0) + .within(() => { + cy.contains('Search for a pack to run'); + cy.contains('Pack is a required field'); + cy.getBySel('comboBoxInput').type(`${packName}{downArrow}{enter}`); + }) + .clickOutside(); cy.getBySel(RESPONSE_ACTIONS_ITEM_1).within(() => { cy.contains('select * from uptime'); cy.contains('Custom key/value pairs. e.g. {"application":"foo-bar","env":"production"}'); @@ -126,6 +134,7 @@ describe('Alert Event Details - Response Actions Form', { tags: ['@ess', '@serve }); cy.intercept('PUT', '/api/detection_engine/rules').as('saveRuleSingleQuery'); + cy.getBySel('ruleEditSubmitButton').click(); cy.wait('@saveRuleSingleQuery').should(({ request }) => { const oneQuery = [ @@ -141,8 +150,10 @@ describe('Alert Event Details - Response Actions Form', { tags: ['@ess', '@serve cy.contains(`${ruleName} was saved`).should('exist'); closeToastIfVisible(); + cy.getBySel('globalLoadingIndicator').should('not.exist'); cy.getBySel('editRuleSettingsLink').click(); cy.getBySel('globalLoadingIndicator').should('not.exist'); + cy.getBySel('edit-rule-actions-tab').click(); cy.getBySel(RESPONSE_ACTIONS_ITEM_0) .within(() => { @@ -153,12 +164,15 @@ describe('Alert Event Details - Response Actions Form', { tags: ['@ess', '@serve }) .clickOutside(); - cy.getBySel(RESPONSE_ACTIONS_ITEM_1).within(() => { - cy.contains('select * from uptime'); - cy.contains('Custom key/value pairs. e.g. {"application":"foo-bar","env":"production"}'); - cy.contains('Days of uptime'); - }); + cy.getBySel(RESPONSE_ACTIONS_ITEM_1) + .within(() => { + cy.contains('select * from uptime'); + cy.contains('Custom key/value pairs. e.g. {"application":"foo-bar","env":"production"}'); + cy.contains('Days of uptime'); + }) + .clickOutside(); cy.intercept('PUT', '/api/detection_engine/rules').as('saveRuleMultiQuery'); + cy.contains('Save changes').click(); cy.wait('@saveRuleMultiQuery').should(({ request }) => { const threeQueries = [ diff --git a/x-pack/plugins/osquery/cypress/e2e/all/cases.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/cases.cy.ts index 72d4adcbe2669..1c6a96bfccb4f 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/cases.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/cases.cy.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { initializeDataViews } from '../../tasks/login'; import { addLiveQueryToCase, checkActionItemsInResults, @@ -14,11 +15,11 @@ import { navigateTo } from '../../tasks/navigation'; import { loadLiveQuery, loadCase, cleanupCase } from '../../tasks/api_fixtures'; import { ServerlessRoleName } from '../../support/roles'; -// FLAKY: https://github.com/elastic/kibana/issues/169747 -describe.skip('Add to Cases', () => { +describe('Add to Cases', () => { let liveQueryId: string; let liveQueryQuery: string; before(() => { + initializeDataViews(); loadLiveQuery({ agent_all: true, query: "SELECT * FROM os_version where name='Ubuntu';", @@ -32,7 +33,7 @@ describe.skip('Add to Cases', () => { describe('observability', { tags: ['@ess'] }, () => { let caseId: string; let caseTitle: string; - before(() => { + beforeEach(() => { loadCase('observability').then((caseInfo) => { caseId = caseInfo.id; caseTitle = caseInfo.title; @@ -41,7 +42,7 @@ describe.skip('Add to Cases', () => { navigateTo('/app/osquery'); }); - after(() => { + afterEach(() => { cleanupCase(caseId); }); @@ -64,7 +65,7 @@ describe.skip('Add to Cases', () => { let caseId: string; let caseTitle: string; - before(() => { + beforeEach(() => { loadCase('securitySolution').then((caseInfo) => { caseId = caseInfo.id; caseTitle = caseInfo.title; @@ -73,7 +74,7 @@ describe.skip('Add to Cases', () => { navigateTo('/app/osquery'); }); - after(() => { + afterEach(() => { cleanupCase(caseId); }); diff --git a/x-pack/plugins/osquery/cypress/e2e/all/custom_space.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/custom_space.cy.ts index edee4bdb0c8b1..ba85ec700ccc2 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/custom_space.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/custom_space.cy.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { initializeDataViews } from '../../tasks/login'; import { navigateTo } from '../../tasks/navigation'; import { checkActionItemsInResults, @@ -28,6 +29,7 @@ describe('ALL - Custom space', () => { let spaceId: string; before(() => { + initializeDataViews(); cy.wrap( new Promise((resolve) => { if (testSpace.name !== 'default') { diff --git a/x-pack/plugins/osquery/cypress/e2e/all/ecs_mappings.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/ecs_mappings.cy.ts index 98ad7ad0a26c2..069753f96fa9a 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/ecs_mappings.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/ecs_mappings.cy.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { initializeDataViews } from '../../tasks/login'; import { getAdvancedButton } from '../../screens/integrations'; import { navigateTo } from '../../tasks/navigation'; import { @@ -19,6 +20,10 @@ import { import { ServerlessRoleName } from '../../support/roles'; describe('EcsMapping', { tags: ['@ess', '@serverless'] }, () => { + before(() => { + initializeDataViews(); + }); + beforeEach(() => { cy.login(ServerlessRoleName.SOC_MANAGER); }); diff --git a/x-pack/plugins/osquery/cypress/e2e/all/live_query.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/live_query.cy.ts index dd8aaef9149f5..21308b52ba057 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/live_query.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/live_query.cy.ts @@ -18,8 +18,7 @@ import { LIVE_QUERY_EDITOR } from '../../screens/live_query'; import { getAdvancedButton } from '../../screens/integrations'; import { ServerlessRoleName } from '../../support/roles'; -// FLAKY: https://github.com/elastic/kibana/issues/169725 -describe.skip('ALL - Live Query', { tags: ['@ess', '@serverless'] }, () => { +describe('ALL - Live Query', { tags: ['@ess', '@serverless'] }, () => { beforeEach(() => { cy.login(ServerlessRoleName.SOC_MANAGER); navigateTo('/app/osquery'); @@ -87,6 +86,6 @@ describe.skip('ALL - Live Query', { tags: ['@ess', '@serverless'] }, () => { inputQuery('{selectall}{backspace}{selectall}{backspace}'); // not sure if this is how it used to work when I implemented the functionality, but let's leave it like this for now - cy.get(LIVE_QUERY_EDITOR).invoke('height').should('be.gt', 200).and('be.lt', 380); + cy.get(LIVE_QUERY_EDITOR).invoke('height').should('be.gt', 200).and('be.lt', 400); }); }); diff --git a/x-pack/plugins/osquery/cypress/e2e/all/metrics.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/metrics.cy.ts index 70614f958bb2f..e0442be19ece5 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/metrics.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/metrics.cy.ts @@ -11,9 +11,7 @@ import { loadSavedQuery, cleanupSavedQuery } from '../../tasks/api_fixtures'; import { triggerLoadData } from '../../tasks/inventory'; import { ServerlessRoleName } from '../../support/roles'; -// FLAKY: https://github.com/elastic/kibana/issues/169574 -// FLAKY: https://github.com/elastic/kibana/issues/169575 -describe.skip('ALL - Inventory', { tags: ['@ess'] }, () => { +describe('ALL - Inventory', { tags: ['@ess'] }, () => { let savedQueryName: string; let savedQueryId: string; diff --git a/x-pack/plugins/osquery/cypress/e2e/all/packs_create_edit.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/packs_create_edit.cy.ts index de71f491cf213..922a5b7471eb7 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/packs_create_edit.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/packs_create_edit.cy.ts @@ -15,7 +15,7 @@ import { findFormFieldByRowsLabelAndType, inputQuery, } from '../../tasks/live_query'; -import { activatePack, deactivatePack, preparePack } from '../../tasks/packs'; +import { changePackActiveStatus, preparePack } from '../../tasks/packs'; import { closeModalIfVisible, closeToastIfVisible, @@ -513,8 +513,8 @@ describe('Packs - Create and Edit', { tags: ['@ess', '@serverless'] }, () => { it('', () => { cy.contains('Packs').click(); - deactivatePack(packName); - activatePack(packName); + changePackActiveStatus(packName); + changePackActiveStatus(packName); }); }); diff --git a/x-pack/plugins/osquery/cypress/e2e/all/packs_integration.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/packs_integration.cy.ts index 0ba8d631905f9..a9222abc60c10 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/packs_integration.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/packs_integration.cy.ts @@ -8,17 +8,15 @@ import { find } from 'lodash'; import type { PackagePolicy } from '@kbn/fleet-plugin/common'; import { API_VERSIONS } from '../../../common/constants'; -import { FLEET_AGENT_POLICIES, navigateTo } from '../../tasks/navigation'; +import { FLEET_AGENT_POLICIES, navigateToWithoutWaitForReact } from '../../tasks/navigation'; import { checkActionItemsInResults, checkResults, deleteAndConfirm, - findAndClickButton, - findFormFieldByRowsLabelAndType, selectAllAgents, submitQuery, } from '../../tasks/live_query'; -import { activatePack, cleanupAllPrebuiltPacks, deactivatePack } from '../../tasks/packs'; +import { changePackActiveStatus, cleanupAllPrebuiltPacks } from '../../tasks/packs'; import { addIntegration, closeModalIfVisible, @@ -62,19 +60,19 @@ describe('ALL - Packs', { tags: ['@ess', '@serverless'] }, () => { cy.contains(integration).click(); addIntegration(AGENT_POLICY_NAME); cy.contains('Add Elastic Agent later').click(); - navigateTo('app/osquery/packs'); - findAndClickButton('Add pack'); - findFormFieldByRowsLabelAndType('Name', REMOVING_PACK); - findFormFieldByRowsLabelAndType('Scheduled agent policies (optional)', AGENT_POLICY_NAME); - findAndClickButton('Save pack'); - + navigateToWithoutWaitForReact('app/osquery/packs'); + cy.getBySel('addPackButton').click(); + cy.get('input[name="name"]').type(`${REMOVING_PACK}{downArrow}{enter}`); + cy.getBySel('policyIdsComboBox').type(`${AGENT_POLICY_NAME}{downArrow}{enter}`); + cy.getBySel('savePackButton').click(); closeToastIfVisible(); cy.getBySel('tablePaginationPopoverButton').click(); cy.getBySel('tablePagination-50-rows').click(); - cy.react('ScheduledQueryNameComponent', { props: { name: REMOVING_PACK } }).click(); + cy.contains(REMOVING_PACK).click(); cy.contains(`${REMOVING_PACK} details`).should('exist'); - findAndClickButton('Edit'); - cy.react('EuiComboBoxInput', { props: { value: AGENT_POLICY_NAME } }).should('exist'); + cy.get('span').contains('Edit').click(); + + cy.getBySel('comboBoxInput').contains(AGENT_POLICY_NAME).should('exist'); cy.visit(FLEET_AGENT_POLICIES); cy.contains(AGENT_POLICY_NAME).click(); @@ -86,12 +84,13 @@ describe('ALL - Packs', { tags: ['@ess', '@serverless'] }, () => { cy.contains(/^Delete integration$/).click(); closeModalIfVisible(); cy.contains(/^Deleted integration 'osquery_manager-*/); - navigateTo('app/osquery/packs'); + navigateToWithoutWaitForReact('app/osquery/packs'); cy.contains(REMOVING_PACK).click(); cy.contains(`${REMOVING_PACK} details`).should('exist'); cy.wait(1000); - findAndClickButton('Edit'); - cy.react('EuiComboBoxInput', { props: { value: '' } }).should('exist'); + cy.get('span').contains('Edit').click(); + + cy.getBySel('comboBoxInput').should('have.value', ''); }); } ); @@ -100,49 +99,44 @@ describe('ALL - Packs', { tags: ['@ess', '@serverless'] }, () => { afterEach(() => { cleanupAllPrebuiltPacks(); }); - const PREBUILD_PACK_NAME = 'it-compliance'; describe('', () => { beforeEach(() => { cy.login(ServerlessRoleName.SOC_MANAGER); - navigateTo('/app/osquery/packs'); + navigateToWithoutWaitForReact('/app/osquery/packs'); }); it('should load prebuilt packs', () => { cy.contains('Load Elastic prebuilt packs').click(); cy.contains('Load Elastic prebuilt packs').should('not.exist'); cy.wait(1000); - cy.react('EuiTableRow').should('have.length.above', 5); + cy.get('tbody > tr').should('have.length.above', 5); }); it('should be able to activate pack', () => { - activatePack(PREBUILD_PACK_NAME); - deactivatePack(PREBUILD_PACK_NAME); + changePackActiveStatus(PREBUILD_PACK_NAME); + changePackActiveStatus(PREBUILD_PACK_NAME); }); it('should be able to add policy to it', () => { cy.contains(PREBUILD_PACK_NAME).click(); cy.contains('Edit').click(); - findFormFieldByRowsLabelAndType( - 'Scheduled agent policies (optional)', - `${DEFAULT_POLICY} {downArrow}{enter}` - ); - cy.contains('Update pack').click(); + cy.getBySel('policyIdsComboBox').type(`${DEFAULT_POLICY} {downArrow}{enter}`); + cy.getBySel('updatePackButton').click(); cy.getBySel('confirmModalConfirmButton').click(); cy.contains(`Successfully updated "${PREBUILD_PACK_NAME}" pack`); }); it('should be able to activate pack with agent inside', () => { - activatePack(PREBUILD_PACK_NAME); - deactivatePack(PREBUILD_PACK_NAME); + changePackActiveStatus(PREBUILD_PACK_NAME); + changePackActiveStatus(PREBUILD_PACK_NAME); }); it('should not be able to update prebuilt pack', () => { cy.contains(PREBUILD_PACK_NAME).click(); cy.contains('Edit').click(); - cy.react('EuiFieldText', { props: { name: 'name', isDisabled: true } }); - cy.react('EuiFieldText', { props: { name: 'description', isDisabled: true } }); + cy.get('input[name="name"]').should('be.disabled'); + cy.get('input[name="description"]').should('be.disabled'); cy.contains('Add Query').should('not.exist'); - cy.react('ExpandedItemActions', { options: { timeout: 1000 } }); cy.get('.euiTableRowCell--hasActions').should('not.exist'); }); it('should be able to delete prebuilt pack and add it again', () => { @@ -156,7 +150,7 @@ describe('ALL - Packs', { tags: ['@ess', '@serverless'] }, () => { }); it('should be able to run live prebuilt pack', () => { - navigateTo('/app/osquery/live_queries'); + navigateToWithoutWaitForReact('/app/osquery/live_queries'); cy.contains('New live query').click(); cy.contains('Run a set of queries in a pack.').click(); cy.get(LIVE_QUERY_EDITOR).should('not.exist'); @@ -171,17 +165,16 @@ describe('ALL - Packs', { tags: ['@ess', '@serverless'] }, () => { cases: true, timeline: false, }); - navigateTo('/app/osquery'); + navigateToWithoutWaitForReact('/app/osquery'); cy.contains('osquery-monitoring'); }); }); }); - // FLAKY: https://github.com/elastic/kibana/issues/169688 - describe.skip('Global packs', { tags: ['@ess', '@serverless'] }, () => { + describe('Global packs', { tags: ['@ess', '@serverless'] }, () => { beforeEach(() => { cy.login(ServerlessRoleName.PLATFORM_ENGINEER); - navigateTo('/app/osquery/packs'); + navigateToWithoutWaitForReact('/app/osquery/packs'); }); describe('add proper shard to policies packs config', () => { @@ -205,14 +198,12 @@ describe('ALL - Packs', { tags: ['@ess', '@serverless'] }, () => { }); it('add global packs to policies', () => { - findAndClickButton('Add pack'); - findFormFieldByRowsLabelAndType('Name', globalPack); + cy.getBySel('addPackButton').click(); + cy.get('input[name="name"]').type(`${globalPack}{downArrow}{enter}`); cy.getBySel('policyIdsComboBox').should('exist'); cy.getBySel('osqueryPackTypeGlobal').click(); cy.getBySel('policyIdsComboBox').should('not.exist'); - - findAndClickButton('Save pack'); - + cy.getBySel('savePackButton').click(); cy.getBySel('tablePaginationPopoverButton').click(); cy.getBySel('tablePagination-50-rows').click(); cy.contains(globalPack); @@ -269,8 +260,8 @@ describe('ALL - Packs', { tags: ['@ess', '@serverless'] }, () => { it('', () => { const shardPack = 'shardPack' + generateRandomStringName(1)[0]; - findAndClickButton('Add pack'); - findFormFieldByRowsLabelAndType('Name', shardPack); + cy.getBySel('addPackButton').click(); + cy.get('input[name="name"]').type(`${shardPack}{downArrow}{enter}`); cy.contains('Partial deployment (shards)').click(); cy.getBySel('packShardsForm-0').within(() => { @@ -281,7 +272,7 @@ describe('ALL - Packs', { tags: ['@ess', '@serverless'] }, () => { cy.getBySel('shards-field-policy').type(`${OSQUERY_POLICY}{downArrow}{enter}`); cy.get('#shardsPercentage1').type('{backspace}{backspace}{backspace}'); }); - findAndClickButton('Save pack'); + cy.getBySel('savePackButton').click(); cy.contains(`Successfully created "${shardPack}" pack`); closeToastIfVisible(); @@ -319,9 +310,9 @@ describe('ALL - Packs', { tags: ['@ess', '@serverless'] }, () => { cy.get('[data-test-subj="packShardsForm-1"]').within(() => { cy.get(`[aria-label="Delete shards row"]`).click(); }); - cy.getBySel('comboBoxInput').contains(OSQUERY_POLICY).should('not.exist'); - cy.getBySel('policyIdsComboBox').click(); - cy.contains(OSQUERY_POLICY).should('exist'); + cy.getBySel('policyIdsComboBox').contains(OSQUERY_POLICY).should('not.exist'); + cy.getBySel('policyIdsComboBox').click().type(`${OSQUERY_POLICY}{downArrow}{enter}`); + cy.getBySel('policyIdsComboBox').contains(OSQUERY_POLICY).should('exist'); }); }); }); diff --git a/x-pack/plugins/osquery/cypress/e2e/all/saved_queries.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/saved_queries.cy.ts index c7528a1410d87..dd812d25d6bc2 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/saved_queries.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/saved_queries.cy.ts @@ -105,18 +105,14 @@ describe('ALL - Saved queries', { tags: ['@ess', '@serverless'] }, () => { }); it('checks result type on prebuilt saved query', () => { - cy.react('CustomItemAction', { - props: { index: 1, item: { id: 'users_elastic' } }, - }).click(); + cy.get(`[aria-label="Edit users_elastic"]`).click(); cy.getBySel('resultsTypeField').within(() => { cy.contains('Snapshot'); }); }); it('user can run prebuilt saved query and add to case', () => { - cy.react('PlayButtonComponent', { - props: { savedQuery: { id: 'users_elastic' } }, - }).click(); + cy.get(`[aria-label="Run users_elastic"]`).click(); selectAllAgents(); submitQuery(); @@ -126,9 +122,7 @@ describe('ALL - Saved queries', { tags: ['@ess', '@serverless'] }, () => { }); it('user can not delete prebuilt saved query but can delete normal saved query', () => { - cy.react('CustomItemAction', { - props: { index: 1, item: { id: 'users_elastic' } }, - }).click(); + cy.get(`[aria-label="Edit users_elastic"]`).click(); cy.contains('Delete query').should('not.exist'); navigateTo(`/app/osquery/saved_queries/${savedQueryId}`); @@ -142,18 +136,14 @@ describe('ALL - Saved queries', { tags: ['@ess', '@serverless'] }, () => { findAndClickButton('Add query'); cy.contains('Attach next query'); - cy.react('EuiComboBox', { - props: { placeholder: 'Search for a query to run, or write a new query below' }, - }) - .click() - .type('users_elastic{downArrow} {enter}'); + cy.getBySel('savedQuerySelect').click().type('users_elastic{downArrow} {enter}'); inputQuery('where name=1'); cy.getBySel('resultsTypeField').click(); cy.contains('Differential (Ignore removals)').click(); cy.contains('Unique identifier of the us').should('exist'); cy.contains('User ID').should('exist'); - cy.react('EuiFlyoutBody').within(() => { + cy.get(`[aria-labelledby="flyoutTitle"]`).within(() => { cy.getBySel('ECSMappingEditorForm') .first() .within(() => { @@ -162,16 +152,15 @@ describe('ALL - Saved queries', { tags: ['@ess', '@serverless'] }, () => { }); cy.contains('Unique identifier of the us').should('not.exist'); cy.contains('User ID').should('not.exist'); - cy.react('EuiFlyoutFooter').react('EuiButton').contains('Save').click(); + cy.get(`[aria-labelledby="flyoutTitle"]`).contains('Save').click(); + + cy.get(`[aria-label="Edit users_elastic"]`).click(); - cy.react('CustomItemAction', { - props: { index: 0, item: { id: 'users_elastic' } }, - }).click(); cy.contains('SELECT * FROM users;where name=1'); cy.contains('Unique identifier of the us.').should('not.exist'); cy.contains('User ID').should('not.exist'); cy.contains('Differential (Ignore removals)').should('exist'); - cy.react('EuiFlyoutFooter').react('EuiButtonEmpty').contains('Cancel').click(); + cy.get(`[aria-labelledby="flyoutTitle"]`).contains('Cancel').click(); }); }); }); diff --git a/x-pack/plugins/osquery/cypress/e2e/all/timelines.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/timelines.cy.ts index 08a1c0925ffb9..6c2380664ba4d 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/timelines.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/timelines.cy.ts @@ -5,10 +5,14 @@ * 2.0. */ +import { initializeDataViews } from '../../tasks/login'; import { takeOsqueryActionWithParams } from '../../tasks/live_query'; import { ServerlessRoleName } from '../../support/roles'; describe.skip('ALL - Timelines', { tags: ['@ess'] }, () => { + before(() => { + initializeDataViews(); + }); beforeEach(() => { cy.login(ServerlessRoleName.SOC_MANAGER); }); diff --git a/x-pack/plugins/osquery/cypress/e2e/roles/alert_test.cy.ts b/x-pack/plugins/osquery/cypress/e2e/roles/alert_test.cy.ts index 38352c49c6459..718c2f32fd581 100644 --- a/x-pack/plugins/osquery/cypress/e2e/roles/alert_test.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/roles/alert_test.cy.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { initializeDataViews } from '../../tasks/login'; import { checkResults, clickRuleName, submitQuery } from '../../tasks/live_query'; import { loadRule, cleanupRule } from '../../tasks/api_fixtures'; import { ServerlessRoleName } from '../../support/roles'; @@ -14,6 +15,7 @@ describe('Alert Test', { tags: ['@ess'] }, () => { let ruleId: string; before(() => { + initializeDataViews(); loadRule().then((data) => { ruleName = data.name; ruleId = data.id; diff --git a/x-pack/plugins/osquery/cypress/serverless_cypress.config.ts b/x-pack/plugins/osquery/cypress/serverless_cypress.config.ts index 506204de66aa2..fac363698170a 100644 --- a/x-pack/plugins/osquery/cypress/serverless_cypress.config.ts +++ b/x-pack/plugins/osquery/cypress/serverless_cypress.config.ts @@ -6,6 +6,7 @@ */ import { defineCypressConfig } from '@kbn/cypress-config'; +import { getFailedSpecVideos } from './support/filter_videos'; import { setupUserDataLoader } from '../../../test_serverless/functional/test_suites/security/cypress/support/setup_data_loader_tasks'; // eslint-disable-next-line import/no-default-export @@ -21,7 +22,9 @@ export default defineCypressConfig({ responseTimeout: 60000, screenshotsFolder: '../../../target/kibana-osquery/cypress/screenshots', trashAssetsBeforeRuns: false, - video: false, + video: true, + videosFolder: '../../../target/kibana-osquery/cypress/videos', + videoCompression: 15, viewportHeight: 946, viewportWidth: 1680, @@ -40,6 +43,7 @@ export default defineCypressConfig({ numTestsKeptInMemory: 3, setupNodeEvents: (on, config) => { setupUserDataLoader(on, config, { additionalRoleName: 'viewer' }); + on('after:spec', getFailedSpecVideos); return config; }, diff --git a/x-pack/plugins/osquery/cypress/support/e2e.ts b/x-pack/plugins/osquery/cypress/support/e2e.ts index 56bcab0e700ad..d5943e655e5ae 100644 --- a/x-pack/plugins/osquery/cypress/support/e2e.ts +++ b/x-pack/plugins/osquery/cypress/support/e2e.ts @@ -23,6 +23,7 @@ // *********************************************************** // force ESM in this module + export {}; // @ts-expect-error ts(2306) module has some interesting ways of importing, see https://github.com/cypress-io/cypress/blob/0871b03c5b21711cd23056454da8f23dcaca4950/npm/grep/README.md#support-file @@ -33,7 +34,7 @@ registerCypressGrep(); import type { SecuritySolutionDescribeBlockFtrConfig } from '@kbn/security-solution-plugin/scripts/run_cypress/utils'; import { login } from '@kbn/security-solution-plugin/public/management/cypress/tasks/login'; -import { ServerlessRoleName } from './roles'; +import type { ServerlessRoleName } from './roles'; import 'cypress-react-selector'; import { waitUntil } from '../tasks/wait_until'; @@ -92,12 +93,3 @@ Cypress.Commands.add('waitUntil', waitUntil); // Alternatively you can use CommonJS syntax: // require('./commands') Cypress.on('uncaught:exception', () => false); - -// Login as a SOC_MANAGER to properly initialize Security Solution App -before(() => { - cy.login(ServerlessRoleName.SOC_MANAGER); - cy.visit('/app/security/alerts'); - cy.getBySel('globalLoadingIndicator').should('exist'); - cy.getBySel('globalLoadingIndicator').should('not.exist'); - cy.getBySel('manage-alert-detection-rules').should('exist'); -}); diff --git a/x-pack/plugins/osquery/cypress/support/filter_videos.ts b/x-pack/plugins/osquery/cypress/support/filter_videos.ts new file mode 100644 index 0000000000000..69826fb39b0bc --- /dev/null +++ b/x-pack/plugins/osquery/cypress/support/filter_videos.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import fs from 'fs'; + +// makes sure we save videos just for failed specs +export const getFailedSpecVideos = (spec: Cypress.Spec, results: CypressCommandLine.RunResult) => { + if (results && results.video) { + // Do we have failures for any retry attempts? + const failures = results.tests.some((test) => + test.attempts.some((attempt) => attempt.state === 'failed') + ); + if (!failures) { + // delete the video if the spec passed and no tests retried + fs.unlinkSync(results.video); + } + } +}; diff --git a/x-pack/plugins/osquery/cypress/tasks/integrations.ts b/x-pack/plugins/osquery/cypress/tasks/integrations.ts index d1020c51db9a3..760f9da287dba 100644 --- a/x-pack/plugins/osquery/cypress/tasks/integrations.ts +++ b/x-pack/plugins/osquery/cypress/tasks/integrations.ts @@ -54,7 +54,8 @@ export const integrationExistsWithinPolicyDetails = (integrationName: string) => }; export const interceptAgentPolicyId = (cb: (policyId: string) => void) => { - cy.intercept('POST', '**/api/fleet/agent_policies**', (req) => { + // create policy has agent_policies?SOMEPARAMS=true , this ? helps to distinguish it from the delete agent_policies/delete route + cy.intercept('POST', '**/api/fleet/agent_policies?**', (req) => { req.continue((res) => { cb(res.body.item.id); diff --git a/x-pack/plugins/osquery/cypress/tasks/inventory.ts b/x-pack/plugins/osquery/cypress/tasks/inventory.ts index 8ba6fc0702d21..d4a2d1111db8f 100644 --- a/x-pack/plugins/osquery/cypress/tasks/inventory.ts +++ b/x-pack/plugins/osquery/cypress/tasks/inventory.ts @@ -9,7 +9,15 @@ export const triggerLoadData = () => { cy.getBySel('infraWaffleTimeControlsAutoRefreshButton').should('exist'); cy.wait(1000); cy.getBySel('infraWaffleTimeControlsAutoRefreshButton').click(); - cy.getBySel('nodeContainer').last().should('exist'); + // @ts-expect-error update types for multiple true + cy.getBySel('nodeContainer', { multiple: true }) + .not(':contains("dev-fleet-server")') + .first() + .should('exist'); cy.getBySel('infraWaffleTimeControlsStopRefreshingButton').click(); - cy.getBySel('nodeContainer').last().click(); + // @ts-expect-error update types for multiple true + cy.getBySel('nodeContainer', { multiple: true }) + .not(':contains("dev-fleet-server")') + .first() + .click(); }; diff --git a/x-pack/plugins/osquery/cypress/tasks/live_query.ts b/x-pack/plugins/osquery/cypress/tasks/live_query.ts index 2530eeaf45c7b..524f721de11ee 100644 --- a/x-pack/plugins/osquery/cypress/tasks/live_query.ts +++ b/x-pack/plugins/osquery/cypress/tasks/live_query.ts @@ -80,9 +80,9 @@ export const findFormFieldByRowsLabelAndType = (label: string, text: string) => }; export const deleteAndConfirm = (type: string) => { - cy.react('EuiButton').contains(`Delete ${type}`).click(); + cy.get('span').contains(`Delete ${type}`).click(); cy.contains(`Are you sure you want to delete this ${type}?`); - cy.react('EuiButton').contains('Confirm').click(); + cy.get('span').contains('Confirm').click(); cy.get('[data-test-subj="globalToastList"]') .first() .contains('Successfully deleted') diff --git a/x-pack/plugins/osquery/cypress/tasks/login.ts b/x-pack/plugins/osquery/cypress/tasks/login.ts new file mode 100644 index 0000000000000..89dab4ca72e56 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/tasks/login.ts @@ -0,0 +1,17 @@ +/* + * 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 { ServerlessRoleName } from '../support/roles'; + +// Login as a SOC_MANAGER to properly initialize Security Solution App +export const initializeDataViews = () => { + cy.login(ServerlessRoleName.SOC_MANAGER); + cy.visit('/app/security/alerts'); + cy.getBySel('globalLoadingIndicator').should('exist'); + cy.getBySel('globalLoadingIndicator').should('not.exist'); + cy.getBySel('manage-alert-detection-rules').should('exist'); +}; diff --git a/x-pack/plugins/osquery/cypress/tasks/navigation.ts b/x-pack/plugins/osquery/cypress/tasks/navigation.ts index b6acc081feccd..7a4b9df573370 100644 --- a/x-pack/plugins/osquery/cypress/tasks/navigation.ts +++ b/x-pack/plugins/osquery/cypress/tasks/navigation.ts @@ -24,6 +24,19 @@ export const navigateTo = (page: string, opts?: Partial) = waitForReact(); }; +// We're moving away from using react-cypress-selector, I'll be adjusting this on file by file approach +export const navigateToWithoutWaitForReact = ( + page: string, + opts?: Partial +) => { + cy.visit(page, opts); + cy.contains('Loading Elastic').should('exist'); + cy.contains('Loading Elastic').should('not.exist'); + + // There's a security warning toast that seemingly makes ui elements in the bottom right unavailable, so we close it + closeToastIfVisible(); +}; + export const waitForReact = () => { cy.waitForReact( 10000, diff --git a/x-pack/plugins/osquery/cypress/tasks/packs.ts b/x-pack/plugins/osquery/cypress/tasks/packs.ts index f8bd23f2c7b10..98d10e39c1813 100644 --- a/x-pack/plugins/osquery/cypress/tasks/packs.ts +++ b/x-pack/plugins/osquery/cypress/tasks/packs.ts @@ -20,26 +20,16 @@ export const preparePack = (packName: string) => { createdPack.click(); }; -export const deactivatePack = (packName: string) => { - cy.react('ActiveStateSwitchComponent', { - props: { item: { name: packName } }, - }).click(); - closeModalIfVisible(); +export const changePackActiveStatus = (packName: string) => { + const regex = new RegExp(`Successfully (activated|deactivated) "${packName}" pack`); - cy.contains(`Successfully deactivated "${packName}" pack`).should('not.exist'); - cy.contains(`Successfully deactivated "${packName}" pack`).should('exist'); - closeToastIfVisible(); -}; - -export const activatePack = (packName: string) => { - cy.react('ActiveStateSwitchComponent', { - props: { item: { name: packName } }, - }).click(); + cy.getBySel('globalLoadingIndicator').should('not.exist'); + cy.get(`[aria-label="${packName}"]`).click(); closeModalIfVisible(); - - cy.contains(`Successfully activated "${packName}" pack`).should('not.exist'); - cy.contains(`Successfully activated "${packName}" pack`).should('exist'); + cy.contains(regex).should('not.exist'); + cy.contains(regex).should('exist'); closeToastIfVisible(); + cy.contains(regex).should('not.exist'); }; export const cleanupAllPrebuiltPacks = () => { diff --git a/x-pack/plugins/osquery/cypress/tasks/wait_until.ts b/x-pack/plugins/osquery/cypress/tasks/wait_until.ts index 30df5bc0708fe..361a371865dcb 100644 --- a/x-pack/plugins/osquery/cypress/tasks/wait_until.ts +++ b/x-pack/plugins/osquery/cypress/tasks/wait_until.ts @@ -6,7 +6,7 @@ */ export const waitUntil = (fn: () => Cypress.Chainable) => { - const timeout = 90000; + const timeout = 120000; const interval = 5000; let attempts = timeout / interval; diff --git a/x-pack/plugins/osquery/public/packs/active_state_switch.tsx b/x-pack/plugins/osquery/public/packs/active_state_switch.tsx index 7d973e2af82d6..92cd929ae5298 100644 --- a/x-pack/plugins/osquery/public/packs/active_state_switch.tsx +++ b/x-pack/plugins/osquery/public/packs/active_state_switch.tsx @@ -96,6 +96,7 @@ const ActiveStateSwitchComponent: React.FC = ({ item }) checked={!!item.enabled} disabled={!permissions.writePacks || isLoading} showLabel={false} + aria-label={item.name} label="" onChange={handleToggleActiveClick} /> diff --git a/x-pack/plugins/osquery/public/packs/add_pack_button.tsx b/x-pack/plugins/osquery/public/packs/add_pack_button.tsx index 70a97f2354a54..07df6d83c7284 100644 --- a/x-pack/plugins/osquery/public/packs/add_pack_button.tsx +++ b/x-pack/plugins/osquery/public/packs/add_pack_button.tsx @@ -25,6 +25,7 @@ const AddPackButtonComponent: React.FC = ({ fill = {...newQueryLinkProps} iconType="plusInCircle" isDisabled={!permissions.writePacks} + data-test-subj={'addPackButton'} > diff --git a/x-pack/plugins/osquery/public/packs/form/index.tsx b/x-pack/plugins/osquery/public/packs/form/index.tsx index 259e6d0d7b9ee..43420efd47e3f 100644 --- a/x-pack/plugins/osquery/public/packs/form/index.tsx +++ b/x-pack/plugins/osquery/public/packs/form/index.tsx @@ -315,6 +315,7 @@ const PackFormComponent: React.FC = ({ size="m" iconType="save" onClick={handleSaveClick} + data-test-subj={`${editMode ? 'update' : 'save'}PackButton`} > {editMode ? ( { + return [...Array(length)].map(() => Math.random().toString(36)[2]).join(''); +};