From 514db6f08b1069f1d7fab3ff14534a0915e20da3 Mon Sep 17 00:00:00 2001 From: Tomasz Ciecierski Date: Tue, 27 Jun 2023 14:35:41 +0200 Subject: [PATCH] [Defend workflows] Add e2e tests to Automated response actions (#160044) --- .../endpoint_rule_alert_generator.ts | 9 ++ .../data_generators/fleet_action_generator.ts | 1 - .../index_endpoint_fleet_actions.ts | 128 ++++++++++-------- .../data_loaders/index_endpoint_hosts.ts | 14 +- .../common/endpoint/index_data.ts | 4 +- .../public/management/cypress.config.ts | 2 +- .../form.cy.ts} | 27 +++- .../history_log.cy.ts | 90 ++++++++++++ .../no_license.cy.ts | 82 +++++++++++ .../automated_response_actions/results.cy.ts | 85 ++++++++++++ .../cypress/support/data_loaders.ts | 2 + .../plugin_handlers/endpoint_data_loader.ts | 5 +- .../public/management/cypress/tsconfig.json | 1 + .../public/management/cypress/types.ts | 10 +- .../roles_users/with_response_actions_role.ts | 2 + .../without_response_actions_role.ts | 3 +- .../scripts/run_cypress/parallel.ts | 7 +- 17 files changed, 391 insertions(+), 81 deletions(-) rename x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/{response_actions.cy.ts => automated_response_actions/form.cy.ts} (88%) create mode 100644 x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/automated_response_actions/history_log.cy.ts create mode 100644 x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/automated_response_actions/no_license.cy.ts create mode 100644 x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/automated_response_actions/results.cy.ts diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_rule_alert_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_rule_alert_generator.ts index 8396f86a45e97..1b74ae55f6289 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_rule_alert_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_rule_alert_generator.ts @@ -279,6 +279,15 @@ export class EndpointRuleAlertGenerator extends BaseDataGenerator { value: '', }, ], + response_actions: [ + { + action_type_id: 'endpoint', + params: { + command: 'isolate', + comment: 'test', + }, + }, + ], rule_id: ELASTIC_SECURITY_RULE_ID, rule_name_override: 'message', severity: 'medium', diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_action_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_action_generator.ts index 093ea58251687..1b60699e7c19f 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_action_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_action_generator.ts @@ -23,7 +23,6 @@ export class FleetActionGenerator extends BaseDataGenerator { /** Generate a random endpoint Action (isolate or unisolate) */ generate(overrides: DeepPartial = {}): EndpointAction { const timeStamp = overrides['@timestamp'] ? new Date(overrides['@timestamp']) : new Date(); - return merge( { action_id: this.seededUUIDv4(), diff --git a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_fleet_actions.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_fleet_actions.ts index 48587055a8988..a0be011c4ce90 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_fleet_actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_fleet_actions.ts @@ -15,11 +15,13 @@ import type { LogsEndpointAction, LogsEndpointActionResponse, } from '../types'; -import { ENDPOINT_ACTIONS_INDEX, ENDPOINT_ACTION_RESPONSES_INDEX } from '../constants'; +import { ENDPOINT_ACTION_RESPONSES_INDEX, ENDPOINT_ACTIONS_INDEX } from '../constants'; import { FleetActionGenerator } from '../data_generators/fleet_action_generator'; import { wrapErrorAndRejectPromise } from './utils'; +import { EndpointActionGenerator } from '../data_generators/endpoint_action_generator'; -const defaultFleetActionGenerator = new FleetActionGenerator(); +const fleetActionGenerator = new FleetActionGenerator(); +const endpointActionGenerator = new EndpointActionGenerator(); export interface IndexedEndpointAndFleetActionsForHostResponse { actions: EndpointAction[]; @@ -34,25 +36,26 @@ export interface IndexedEndpointAndFleetActionsForHostResponse { export interface IndexEndpointAndFleetActionsForHostOptions { numResponseActions?: number; + alertIds?: string[]; } + /** * Indexes a random number of Endpoint (via Fleet) Actions for a given host - * (NOTE: ensure that fleet is setup first before calling this loading function) + * (NOTE: ensure that fleet is set up first before calling this loading function) * * @param esClient * @param endpointHost - * @param [fleetActionGenerator] + * @param options */ export const indexEndpointAndFleetActionsForHost = async ( esClient: Client, endpointHost: HostMetadata, - fleetActionGenerator: FleetActionGenerator = defaultFleetActionGenerator, options: IndexEndpointAndFleetActionsForHostOptions = {} ): Promise => { const ES_INDEX_OPTIONS = { headers: { 'X-elastic-product-origin': 'fleet' } }; const agentId = endpointHost.elastic.agent.id; const actionsCount = options.numResponseActions ?? 1; - const total = fleetActionGenerator.randomN(5) + actionsCount; + const total = actionsCount === 1 ? actionsCount : fleetActionGenerator.randomN(5) + actionsCount; const response: IndexedEndpointAndFleetActionsForHostResponse = { actions: [], actionResponses: [], @@ -65,49 +68,60 @@ export const indexEndpointAndFleetActionsForHost = async ( }; for (let i = 0; i < total; i++) { - // create an action - const action = fleetActionGenerator.generate({ - data: { comment: 'data generator: this host is bad' }, + // start with endpoint action + const logsEndpointAction: LogsEndpointAction = endpointActionGenerator.generate({ + EndpointActions: { + data: { comment: 'data generator: this host is bad' }, + }, }); - action.agents = [agentId]; + const fleetAction: EndpointAction = { + ...logsEndpointAction.EndpointActions, + '@timestamp': logsEndpointAction['@timestamp'], + agents: + typeof logsEndpointAction.agent.id === 'string' + ? [logsEndpointAction.agent.id] + : logsEndpointAction.agent.id, + user_id: logsEndpointAction.user.id, + }; + + // index fleet action const indexFleetActions = esClient - .index( + .index( { index: AGENT_ACTIONS_INDEX, - body: action, + body: fleetAction, refresh: 'wait_for', }, ES_INDEX_OPTIONS ) .catch(wrapErrorAndRejectPromise); - const endpointActionsBody: LogsEndpointAction & { - EndpointActions: LogsEndpointAction['EndpointActions'] & { - '@timestamp': undefined; - user_id: undefined; - }; - } = { + const logsEndpointActionsBody: LogsEndpointAction = { + ...logsEndpointAction, EndpointActions: { - ...action, - '@timestamp': undefined, - user_id: undefined, - }, - agent: { - id: [agentId], - }, - '@timestamp': action['@timestamp'], - user: { - id: action.user_id, + ...logsEndpointAction.EndpointActions, + data: { + ...logsEndpointAction.EndpointActions.data, + alert_id: options.alertIds, + }, }, + // to test automated actions in cypress + user: options.alertIds ? { id: 'unknown' } : logsEndpointAction.user, + rule: options.alertIds + ? { + id: 'generated_rule_id', + name: 'generated_rule_name', + } + : logsEndpointAction.rule, }; await Promise.all([ indexFleetActions, esClient - .index({ + .index({ index: ENDPOINT_ACTIONS_INDEX, - body: endpointActionsBody, + body: logsEndpointActionsBody, refresh: 'wait_for', }) .catch(wrapErrorAndRejectPromise), @@ -115,8 +129,8 @@ export const indexEndpointAndFleetActionsForHost = async ( const randomFloat = fleetActionGenerator.randomFloat(); // Create an action response for the above - const actionResponse = fleetActionGenerator.generateResponse({ - action_id: action.action_id, + const fleetActionResponse: EndpointActionResponse = fleetActionGenerator.generateResponse({ + action_id: logsEndpointAction.EndpointActions.action_id, agent_id: agentId, action_response: { endpoint: { @@ -129,10 +143,10 @@ export const indexEndpointAndFleetActionsForHost = async ( }); const indexFleetResponses = esClient - .index( + .index( { index: AGENT_ACTIONS_RESULTS_INDEX, - body: actionResponse, + body: fleetActionResponse, refresh: 'wait_for', }, ES_INDEX_OPTIONS @@ -143,8 +157,8 @@ export const indexEndpointAndFleetActionsForHost = async ( if (randomFloat < 0.7) { const endpointActionResponseBody = { EndpointActions: { - ...actionResponse, - data: actionResponse.action_data, + ...fleetActionResponse, + data: fleetActionResponse.action_data, '@timestamp': undefined, action_data: undefined, agent_id: undefined, @@ -157,16 +171,16 @@ export const indexEndpointAndFleetActionsForHost = async ( error: randomFloat < 0.1 ? { - message: actionResponse.error, + message: fleetActionResponse.error, } : undefined, - '@timestamp': actionResponse['@timestamp'], + '@timestamp': fleetActionResponse['@timestamp'], }; await Promise.all([ indexFleetResponses, esClient - .index({ + .index({ index: ENDPOINT_ACTION_RESPONSES_INDEX, body: endpointActionResponseBody, refresh: 'wait_for', @@ -178,8 +192,8 @@ export const indexEndpointAndFleetActionsForHost = async ( await indexFleetResponses; } - response.actions.push(action); - response.actionResponses.push(actionResponse); + response.actions.push(fleetAction); + response.actionResponses.push(fleetActionResponse); } // Add edge case fleet actions (maybe) @@ -191,54 +205,54 @@ export const indexEndpointAndFleetActionsForHost = async ( }; // 70% of the time just add either an Isolate -OR- an UnIsolate action if (randomFloat < 0.7) { - let action: EndpointAction; + let fleetAction: EndpointAction; if (randomFloat < 0.3) { // add a pending isolation - action = fleetActionGenerator.generateIsolateAction(actionStartedAt); + fleetAction = fleetActionGenerator.generateIsolateAction(actionStartedAt); } else { // add a pending UN-isolation - action = fleetActionGenerator.generateUnIsolateAction(actionStartedAt); + fleetAction = fleetActionGenerator.generateUnIsolateAction(actionStartedAt); } - action.agents = [agentId]; + fleetAction.agents = [agentId]; await esClient - .index( + .index( { index: AGENT_ACTIONS_INDEX, - body: action, + body: fleetAction, refresh: 'wait_for', }, ES_INDEX_OPTIONS ) .catch(wrapErrorAndRejectPromise); - response.actions.push(action); + response.actions.push(fleetAction); } else { // Else (30% of the time) add a pending isolate AND pending un-isolate - const action1 = fleetActionGenerator.generateIsolateAction(actionStartedAt); - const action2 = fleetActionGenerator.generateUnIsolateAction(actionStartedAt); + const fleetAction1 = fleetActionGenerator.generateIsolateAction(actionStartedAt); + const fleetAction2 = fleetActionGenerator.generateUnIsolateAction(actionStartedAt); - action1.agents = [agentId]; - action2.agents = [agentId]; + fleetAction1.agents = [agentId]; + fleetAction2.agents = [agentId]; await Promise.all([ esClient - .index( + .index( { index: AGENT_ACTIONS_INDEX, - body: action1, + body: fleetAction1, refresh: 'wait_for', }, ES_INDEX_OPTIONS ) .catch(wrapErrorAndRejectPromise), esClient - .index( + .index( { index: AGENT_ACTIONS_INDEX, - body: action2, + body: fleetAction2, refresh: 'wait_for', }, ES_INDEX_OPTIONS @@ -246,7 +260,7 @@ export const indexEndpointAndFleetActionsForHost = async ( .catch(wrapErrorAndRejectPromise), ]); - response.actions.push(action1, action2); + response.actions.push(fleetAction1, fleetAction2); } } diff --git a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts index c467735fdb327..d778e1cde027f 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts @@ -90,6 +90,7 @@ export async function indexEndpointHostDocs({ generator, withResponseActions = true, numResponseActions, + alertIds, }: { numDocs: number; client: Client; @@ -102,6 +103,7 @@ export async function indexEndpointHostDocs({ generator: EndpointDocGenerator; withResponseActions?: boolean; numResponseActions?: IndexEndpointAndFleetActionsForHostOptions['numResponseActions']; + alertIds?: string[]; }): Promise { const timeBetweenDocs = 6 * 3600 * 1000; // 6 hours between metadata documents const timestamp = new Date().getTime(); @@ -198,14 +200,10 @@ export async function indexEndpointHostDocs({ if (withResponseActions) { // Create some fleet endpoint actions and .logs-endpoint actions for this Host - const actionsResponse = await indexEndpointAndFleetActionsForHost( - client, - hostMetadata, - undefined, - { - numResponseActions, - } - ); + const actionsResponse = await indexEndpointAndFleetActionsForHost(client, hostMetadata, { + alertIds, + numResponseActions, + }); mergeAndAppendArrays(response, actionsResponse); } } diff --git a/x-pack/plugins/security_solution/common/endpoint/index_data.ts b/x-pack/plugins/security_solution/common/endpoint/index_data.ts index 2ad264ab14b91..d01c5f9bdae07 100644 --- a/x-pack/plugins/security_solution/common/endpoint/index_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/index_data.ts @@ -65,7 +65,8 @@ export async function indexHostsAndAlerts( options: TreeOptions = {}, DocGenerator: typeof EndpointDocGenerator = EndpointDocGenerator, withResponseActions = true, - numResponseActions?: number + numResponseActions?: number, + alertIds?: string[] ): Promise { const random = seedrandom(seed); const epmEndpointPackage = await getEndpointPackageInfo(kbnClient); @@ -119,6 +120,7 @@ export async function indexHostsAndAlerts( generator, withResponseActions, numResponseActions, + alertIds, }); mergeAndAppendArrays(response, indexedHosts); diff --git a/x-pack/plugins/security_solution/public/management/cypress.config.ts b/x-pack/plugins/security_solution/public/management/cypress.config.ts index d22255d3dc635..153527a19ea15 100644 --- a/x-pack/plugins/security_solution/public/management/cypress.config.ts +++ b/x-pack/plugins/security_solution/public/management/cypress.config.ts @@ -40,7 +40,7 @@ export default defineCypressConfig({ // baseUrl: To override, set Env. variable `CYPRESS_BASE_URL` baseUrl: 'http://localhost:5601', supportFile: 'public/management/cypress/support/e2e.ts', - specPattern: 'public/management/cypress/e2e/mocked_data/*.cy.{js,jsx,ts,tsx}', + specPattern: 'public/management/cypress/e2e/mocked_data/', experimentalRunAllSpecs: true, setupNodeEvents: (on, config) => { return dataLoaders(on, config); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/response_actions.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/automated_response_actions/form.cy.ts similarity index 88% rename from x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/response_actions.cy.ts rename to x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/automated_response_actions/form.cy.ts index 987d65b0f311f..9f6b380b1fdf3 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/response_actions.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/automated_response_actions/form.cy.ts @@ -13,12 +13,12 @@ import { tryAddingDisabledResponseAction, validateAvailableCommands, visitRuleActions, -} from '../../tasks/response_actions'; -import { cleanupRule, generateRandomStringName, loadRule } from '../../tasks/api_fixtures'; -import { RESPONSE_ACTION_TYPES } from '../../../../../common/detection_engine/rule_response_actions/schemas'; -import { loginWithRole, ROLE } from '../../tasks/login'; +} from '../../../tasks/response_actions'; +import { cleanupRule, generateRandomStringName, loadRule } from '../../../tasks/api_fixtures'; +import { RESPONSE_ACTION_TYPES } from '../../../../../../common/detection_engine/rule_response_actions/schemas'; +import { loginWithRole, ROLE } from '../../../tasks/login'; -describe('Response actions', () => { +describe('Form', () => { describe('User with no access can not create an endpoint response action', () => { before(() => { loginWithRole(ROLE.endpoint_response_actions_no_access); @@ -142,6 +142,23 @@ describe('Response actions', () => { }); }); + describe('User should not see endpoint action when no rbac', () => { + const [ruleName, ruleDescription] = generateRandomStringName(2); + + before(() => { + loginWithRole(ROLE.endpoint_response_actions_no_access); + }); + + it('response actions are disabled', () => { + fillUpNewRule(ruleName, ruleDescription); + cy.getByTestSubj('response-actions-wrapper').within(() => { + cy.getByTestSubj('Endpoint Security-response-action-type-selection-option').should( + 'be.disabled' + ); + }); + }); + }); + describe('User without access can not edit, add nor delete an endpoint response action', () => { let ruleId: string; diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/automated_response_actions/history_log.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/automated_response_actions/history_log.cy.ts new file mode 100644 index 0000000000000..369ee507206b8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/automated_response_actions/history_log.cy.ts @@ -0,0 +1,90 @@ +/* + * 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 { generateRandomStringName } from '@kbn/osquery-plugin/cypress/tasks/integrations'; +import { indexEndpointHosts } from '../../../tasks/index_endpoint_hosts'; +import type { ReturnTypeFromChainable } from '../../../types'; +import { indexEndpointRuleAlerts } from '../../../tasks/index_endpoint_rule_alerts'; + +import { login, ROLE } from '../../../tasks/login'; + +describe('Response actions history page', () => { + let endpointData: ReturnTypeFromChainable | undefined; + let endpointDataWithAutomated: ReturnTypeFromChainable | undefined; + let alertData: ReturnTypeFromChainable | undefined; + const [endpointAgentId, endpointHostname] = generateRandomStringName(2); + + before(() => { + login(ROLE.endpoint_response_actions_access); + + indexEndpointHosts({ numResponseActions: 2 }).then((indexEndpoints) => { + endpointData = indexEndpoints; + }); + indexEndpointRuleAlerts({ + endpointAgentId, + endpointHostname, + endpointIsolated: false, + }).then((indexedAlert) => { + alertData = indexedAlert; + const alertId = alertData.alerts[0]._id; + return indexEndpointHosts({ + numResponseActions: 1, + alertIds: [alertId], + }).then((indexEndpoints) => { + endpointDataWithAutomated = indexEndpoints; + }); + }); + }); + + after(() => { + if (endpointDataWithAutomated) { + endpointDataWithAutomated.cleanup(); + endpointDataWithAutomated = undefined; + } + if (endpointData) { + endpointData.cleanup(); + endpointData = undefined; + } + + if (alertData) { + alertData.cleanup(); + alertData = undefined; + } + }); + + it('enable filtering by type', () => { + cy.visit(`/app/security/administration/response_actions_history`); + + let maxLength: number; + cy.getByTestSubj('response-actions-list').then(($table) => { + maxLength = $table.find('tbody .euiTableRow').length; + cy.get('tbody .euiTableRow').should('have.lengthOf', maxLength); + }); + + cy.getByTestSubj('response-actions-list-type-filter-popoverButton').click(); + cy.getByTestSubj('type-filter-option').contains('Triggered by rule').click(); + cy.getByTestSubj('response-actions-list').within(() => { + cy.get('tbody .euiTableRow').should('have.lengthOf', 1); + cy.get('tbody .euiTableRow').eq(0).contains('Triggered by rule'); + }); + cy.getByTestSubj('type-filter-option').contains('Triggered by rule').click(); + cy.getByTestSubj('response-actions-list').within(() => { + cy.get('tbody .euiTableRow').should('have.lengthOf', maxLength); + }); + cy.getByTestSubj('type-filter-option').contains('Triggered manually').click(); + cy.getByTestSubj('response-actions-list').within(() => { + cy.get('tbody .euiTableRow').should('have.lengthOf', maxLength - 1); + }); + cy.getByTestSubj('type-filter-option').contains('Triggered by rule').click(); + cy.getByTestSubj('response-actions-list').within(() => { + cy.get('tbody .euiTableRow').should('have.lengthOf', maxLength); + cy.get('tbody .euiTableRow').eq(0).contains('Triggered by rule').click(); + }); + // check if we were moved to Rules app after clicking Triggered by rule + cy.getByTestSubj('breadcrumb last').contains('Rules'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/automated_response_actions/no_license.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/automated_response_actions/no_license.cy.ts new file mode 100644 index 0000000000000..4d830c959399a --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/automated_response_actions/no_license.cy.ts @@ -0,0 +1,82 @@ +/* + * 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 { generateRandomStringName } from '@kbn/osquery-plugin/cypress/tasks/integrations'; +import { APP_ALERTS_PATH } from '../../../../../../common/constants'; +import { closeAllToasts } from '../../../tasks/toasts'; +import { fillUpNewRule } from '../../../tasks/response_actions'; +import { login, loginWithRole, ROLE } from '../../../tasks/login'; +import type { ReturnTypeFromChainable } from '../../../types'; +import { indexEndpointHosts } from '../../../tasks/index_endpoint_hosts'; +import { indexEndpointRuleAlerts } from '../../../tasks/index_endpoint_rule_alerts'; + +describe('No License', { env: { ftrConfig: { license: 'basic' } } }, () => { + describe('User cannot use endpoint action in form', () => { + const [ruleName, ruleDescription] = generateRandomStringName(2); + + before(() => { + loginWithRole(ROLE.endpoint_response_actions_access); + }); + + it('response actions are disabled', () => { + fillUpNewRule(ruleName, ruleDescription); + // addEndpointResponseAction(); + cy.getByTestSubj('response-actions-wrapper').within(() => { + cy.getByTestSubj('Endpoint Security-response-action-type-selection-option').should( + 'be.disabled' + ); + }); + }); + }); + + describe('User cannot see results', () => { + let endpointData: ReturnTypeFromChainable | undefined; + let alertData: ReturnTypeFromChainable | undefined; + const [endpointAgentId, endpointHostname] = generateRandomStringName(2); + before(() => { + login(); + indexEndpointRuleAlerts({ + endpointAgentId, + endpointHostname, + endpointIsolated: false, + }).then((indexedAlert) => { + alertData = indexedAlert; + const alertId = alertData.alerts[0]._id; + return indexEndpointHosts({ + withResponseActions: true, + numResponseActions: 1, + alertIds: [alertId], + }).then((indexEndpoints) => { + endpointData = indexEndpoints; + }); + }); + }); + + after(() => { + if (endpointData) { + endpointData.cleanup(); + endpointData = undefined; + } + + if (alertData) { + alertData.cleanup(); + alertData = undefined; + } + }); + it('show the permission denied callout', () => { + cy.visit(APP_ALERTS_PATH); + closeAllToasts(); + cy.getByTestSubj('expand-event').first().click(); + cy.getByTestSubj('response-actions-notification').should('not.have.text', '0'); + cy.getByTestSubj('responseActionsViewTab').click(); + cy.contains('Permission denied'); + cy.contains( + 'To access these results, ask your administrator for Elastic Defend Kibana privileges.' + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/automated_response_actions/results.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/automated_response_actions/results.cy.ts new file mode 100644 index 0000000000000..bb3be124418f8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/automated_response_actions/results.cy.ts @@ -0,0 +1,85 @@ +/* + * 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 { generateRandomStringName } from '@kbn/osquery-plugin/cypress/tasks/integrations'; +import { APP_ALERTS_PATH } from '../../../../../../common/constants'; +import { closeAllToasts } from '../../../tasks/toasts'; +import { indexEndpointHosts } from '../../../tasks/index_endpoint_hosts'; +import type { ReturnTypeFromChainable } from '../../../types'; +import { indexEndpointRuleAlerts } from '../../../tasks/index_endpoint_rule_alerts'; + +import { login, ROLE } from '../../../tasks/login'; + +describe('Results', () => { + let endpointData: ReturnTypeFromChainable | undefined; + let alertData: ReturnTypeFromChainable | undefined; + const [endpointAgentId, endpointHostname] = generateRandomStringName(2); + + before(() => { + indexEndpointRuleAlerts({ + endpointAgentId, + endpointHostname, + endpointIsolated: false, + }).then((indexedAlert) => { + alertData = indexedAlert; + const alertId = alertData.alerts[0]._id; + return indexEndpointHosts({ + withResponseActions: true, + numResponseActions: 1, + alertIds: [alertId], + }).then((indexEndpoints) => { + endpointData = indexEndpoints; + }); + }); + }); + + after(() => { + if (endpointData) { + endpointData.cleanup(); + endpointData = undefined; + } + + if (alertData) { + alertData.cleanup(); + alertData = undefined; + } + }); + + describe('see results when has RBAC', () => { + before(() => { + login(ROLE.endpoint_response_actions_access); + }); + + it('see endpoint action', () => { + cy.visit(APP_ALERTS_PATH); + closeAllToasts(); + cy.getByTestSubj('expand-event').first().click(); + cy.getByTestSubj('response-actions-notification').should('not.have.text', '0'); + cy.getByTestSubj('responseActionsViewTab').click(); + cy.getByTestSubj('endpoint-results-comment'); + cy.contains(/isolate is pending|isolate completed successfully/g); + }); + }); + describe('do not see results results when does not have RBAC', () => { + before(() => { + login(ROLE.endpoint_response_actions_no_access); + }); + + it('show the permission denied callout', () => { + cy.visit(APP_ALERTS_PATH); + closeAllToasts(); + + cy.getByTestSubj('expand-event').first().click(); + cy.getByTestSubj('response-actions-notification').should('not.have.text', '0'); + cy.getByTestSubj('responseActionsViewTab').click(); + cy.contains('Permission denied'); + cy.contains( + 'To access these results, ask your administrator for Elastic Defend Kibana privileges.' + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts b/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts index a782a775df087..32ada46f13127 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts @@ -122,6 +122,7 @@ export const dataLoaders = ( isolation, withResponseActions, numResponseActions, + alertIds, } = options; return cyLoadEndpointDataHandler(esClient, kbnClient, { @@ -131,6 +132,7 @@ export const dataLoaders = ( isolation, withResponseActions, numResponseActions, + alertIds, }); }, diff --git a/x-pack/plugins/security_solution/public/management/cypress/support/plugin_handlers/endpoint_data_loader.ts b/x-pack/plugins/security_solution/public/management/cypress/support/plugin_handlers/endpoint_data_loader.ts index 5f50eacff76c6..32b392ac32696 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/support/plugin_handlers/endpoint_data_loader.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/support/plugin_handlers/endpoint_data_loader.ts @@ -40,6 +40,7 @@ export interface CyLoadEndpointDataOptions numResponseActions?: number; isolation: boolean; bothIsolatedAndNormalEndpoints?: boolean; + alertIds?: string[]; } /** @@ -65,6 +66,7 @@ export const cyLoadEndpointDataHandler = async ( withResponseActions, isolation, numResponseActions, + alertIds, } = options; const DocGenerator = EndpointDocGenerator.custom({ @@ -94,7 +96,8 @@ export const cyLoadEndpointDataHandler = async ( undefined, DocGenerator, withResponseActions, - numResponseActions + numResponseActions, + alertIds ); if (waitUntilTransformed) { diff --git a/x-pack/plugins/security_solution/public/management/cypress/tsconfig.json b/x-pack/plugins/security_solution/public/management/cypress/tsconfig.json index dc0b2e1ca4fd4..c79c48ca3640f 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tsconfig.json +++ b/x-pack/plugins/security_solution/public/management/cypress/tsconfig.json @@ -35,5 +35,6 @@ "@kbn/test", "@kbn/repo-info", "@kbn/data-views-plugin", + "@kbn/osquery-plugin/cypress", ] } diff --git a/x-pack/plugins/security_solution/public/management/cypress/types.ts b/x-pack/plugins/security_solution/public/management/cypress/types.ts index 6c8bc8a04bed3..fecaa33a6a70a 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/types.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/types.ts @@ -42,10 +42,12 @@ export type ReturnTypeFromChainable = C extends Cyp : never; export type IndexEndpointHostsCyTaskOptions = Partial< - { count: number; withResponseActions: boolean; numResponseActions?: number } & Pick< - CyLoadEndpointDataOptions, - 'version' | 'os' | 'isolation' - > + { + count: number; + withResponseActions: boolean; + numResponseActions?: number; + alertIds?: string[]; + } & Pick >; export interface HostActionResponse { diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/roles_users/with_response_actions_role.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/roles_users/with_response_actions_role.ts index c86fbd472adfb..0fed92037e07f 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/roles_users/with_response_actions_role.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/roles_users/with_response_actions_role.ts @@ -23,6 +23,8 @@ export const getWithResponseActionsRole: () => Omit = () => { 'execute_operations_all', 'host_isolation_all', 'process_operations_all', + 'actions_log_management_all', + 'actions_log_management_read', ], }, }, diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/roles_users/without_response_actions_role.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/roles_users/without_response_actions_role.ts index 8e26bc61972cf..4ed5f91df77dd 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/roles_users/without_response_actions_role.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/roles_users/without_response_actions_role.ts @@ -18,6 +18,7 @@ export const getNoResponseActionsRole: () => Omit = () => ({ '.siem-signals-*', '.items-*', '.lists-*', + '.logs-*', ], privileges: ['manage', 'write', 'read', 'view_index_metadata'], }, @@ -55,8 +56,6 @@ export const getNoResponseActionsRole: () => Omit = () => ({ 'event_filters_read', 'policy_management_all', 'policy_management_read', - 'actions_log_management_all', - 'actions_log_management_read', ], stackAlerts: ['all'], }, diff --git a/x-pack/plugins/security_solution/scripts/run_cypress/parallel.ts b/x-pack/plugins/security_solution/scripts/run_cypress/parallel.ts index 7459b7b04581f..3c14adff1ccc4 100644 --- a/x-pack/plugins/security_solution/scripts/run_cypress/parallel.ts +++ b/x-pack/plugins/security_solution/scripts/run_cypress/parallel.ts @@ -179,9 +179,10 @@ export const cli = () => { } return element.value as string; }); + } else if (property.value.type === 'StringLiteral') { + value = property.value.value; } if (key && value) { - // @ts-expect-error acc[key] = value; } return acc; @@ -280,6 +281,10 @@ export const cli = () => { ); } + if (configFromTestFile?.license) { + vars.esTestCluster.license = configFromTestFile.license; + } + if (hasFleetServerArgs) { vars.kbnTestServer.serverArgs.push( `--xpack.fleet.agents.elasticsearch.host=http://${hostRealIp}:${esPort}`