From 73dfd03acfbb6dbda089f1755b954d7cedb2ee4d Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 28 Nov 2023 09:43:55 +0100 Subject: [PATCH 01/14] [Observability] Remove redundant contexts from pages (#171882) --- .../public/application/index.tsx | 5 +- .../has_data_context/has_data_context.tsx | 134 +++++++++--------- .../public/pages/alerts/alerts.tsx | 7 - .../public/pages/cases/cases.tsx | 8 -- .../observability/public/pages/slos/slos.tsx | 6 +- .../observability/public/routes/routes.tsx | 15 +- 6 files changed, 78 insertions(+), 97 deletions(-) diff --git a/x-pack/plugins/observability/public/application/index.tsx b/x-pack/plugins/observability/public/application/index.tsx index 8eb2bfe7df7bb..4a99d3648af34 100644 --- a/x-pack/plugins/observability/public/application/index.tsx +++ b/x-pack/plugins/observability/public/application/index.tsx @@ -20,7 +20,6 @@ import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import { ObservabilityAIAssistantProvider } from '@kbn/observability-ai-assistant-plugin/public'; -import { HasDataContextProvider } from '../context/has_data_context/has_data_context'; import { PluginContext } from '../context/plugin_context/plugin_context'; import { ConfigSchema, ObservabilityPublicPluginsStart } from '../plugin'; import { routes } from '../routes/routes'; @@ -119,9 +118,7 @@ export const renderApp = ({ data-test-subj="observabilityMainContainer" > - - - + diff --git a/x-pack/plugins/observability/public/context/has_data_context/has_data_context.tsx b/x-pack/plugins/observability/public/context/has_data_context/has_data_context.tsx index 40c54e7d0b2ec..9d3b22a27704a 100644 --- a/x-pack/plugins/observability/public/context/has_data_context/has_data_context.tsx +++ b/x-pack/plugins/observability/public/context/has_data_context/has_data_context.tsx @@ -7,7 +7,6 @@ import { isEmpty, uniqueId } from 'lodash'; import React, { createContext, useEffect, useState } from 'react'; -import { useRouteMatch } from 'react-router-dom'; import { asyncForEach } from '@kbn/std'; import { FETCH_STATUS } from '@kbn/observability-shared-plugin/public'; import { useKibana } from '../../utils/kibana_react'; @@ -65,84 +64,81 @@ export function HasDataContextProvider({ children }: { children: React.ReactNode const [hasDataMap, setHasDataMap] = useState({}); - const isExploratoryView = useRouteMatch('/exploratory-view'); - useEffect( () => { - if (!isExploratoryView) - asyncForEach(apps, async (app) => { - try { - const updateState = ({ - hasData, - indices, - serviceName, - }: { - hasData?: boolean; - serviceName?: string; - indices?: string | ApmIndicesConfig; - }) => { - setHasDataMap((prevState) => ({ - ...prevState, - [app]: { - hasData, - ...(serviceName ? { serviceName } : {}), - ...(indices ? { indices } : {}), - status: FETCH_STATUS.SUCCESS, - }, - })); - }; - switch (app) { - case UX_APP: - const params = { absoluteTime: { start: absoluteStart!, end: absoluteEnd! } }; - const resultUx = await getDataHandler(app)?.hasData(params); - updateState({ - hasData: resultUx?.hasData, - indices: resultUx?.indices, - serviceName: resultUx?.serviceName as string, - }); - break; - case UPTIME_APP: - const resultSy = await getDataHandler(app)?.hasData(); - updateState({ hasData: resultSy?.hasData, indices: resultSy?.indices }); - - break; - case APM_APP: - const resultApm = await getDataHandler(app)?.hasData(); - updateState({ hasData: resultApm?.hasData, indices: resultApm?.indices }); - - break; - case INFRA_LOGS_APP: - const resultInfraLogs = await getDataHandler(app)?.hasData(); - updateState({ - hasData: resultInfraLogs?.hasData, - indices: resultInfraLogs?.indices, - }); - break; - case INFRA_METRICS_APP: - const resultInfraMetrics = await getDataHandler(app)?.hasData(); - updateState({ - hasData: resultInfraMetrics?.hasData, - indices: resultInfraMetrics?.indices, - }); - break; - case UNIVERSAL_PROFILING_APP: - // Profiling only shows the empty section for now - updateState({ hasData: false }); - break; - } - } catch (e) { + asyncForEach(apps, async (app) => { + try { + const updateState = ({ + hasData, + indices, + serviceName, + }: { + hasData?: boolean; + serviceName?: string; + indices?: string | ApmIndicesConfig; + }) => { setHasDataMap((prevState) => ({ ...prevState, [app]: { - hasData: undefined, - status: FETCH_STATUS.FAILURE, + hasData, + ...(serviceName ? { serviceName } : {}), + ...(indices ? { indices } : {}), + status: FETCH_STATUS.SUCCESS, }, })); + }; + switch (app) { + case UX_APP: + const params = { absoluteTime: { start: absoluteStart!, end: absoluteEnd! } }; + const resultUx = await getDataHandler(app)?.hasData(params); + updateState({ + hasData: resultUx?.hasData, + indices: resultUx?.indices, + serviceName: resultUx?.serviceName as string, + }); + break; + case UPTIME_APP: + const resultSy = await getDataHandler(app)?.hasData(); + updateState({ hasData: resultSy?.hasData, indices: resultSy?.indices }); + + break; + case APM_APP: + const resultApm = await getDataHandler(app)?.hasData(); + updateState({ hasData: resultApm?.hasData, indices: resultApm?.indices }); + + break; + case INFRA_LOGS_APP: + const resultInfraLogs = await getDataHandler(app)?.hasData(); + updateState({ + hasData: resultInfraLogs?.hasData, + indices: resultInfraLogs?.indices, + }); + break; + case INFRA_METRICS_APP: + const resultInfraMetrics = await getDataHandler(app)?.hasData(); + updateState({ + hasData: resultInfraMetrics?.hasData, + indices: resultInfraMetrics?.indices, + }); + break; + case UNIVERSAL_PROFILING_APP: + // Profiling only shows the empty section for now + updateState({ hasData: false }); + break; } - }); + } catch (e) { + setHasDataMap((prevState) => ({ + ...prevState, + [app]: { + hasData: undefined, + status: FETCH_STATUS.FAILURE, + }, + })); + } + }); }, // eslint-disable-next-line react-hooks/exhaustive-deps - [isExploratoryView] + [] ); useEffect(() => { diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts.tsx index 9e80d01ed82e4..c1b1493daa3d9 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts.tsx @@ -19,12 +19,10 @@ import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; import { rulesLocatorID } from '../../../common'; import { RulesParams } from '../../locators/rules'; import { useKibana } from '../../utils/kibana_react'; -import { useHasData } from '../../hooks/use_has_data'; import { usePluginContext } from '../../hooks/use_plugin_context'; import { useTimeBuckets } from '../../hooks/use_time_buckets'; import { useGetFilteredRuleTypes } from '../../hooks/use_get_filtered_rule_types'; import { useToasts } from '../../hooks/use_toast'; -import { LoadingObservability } from '../../components/loading_observability'; import { renderRuleStats, RuleStatsState } from './components/rule_stats'; import { ObservabilityAlertSearchBar } from '../../components/alert_search_bar/alert_search_bar'; import { @@ -94,7 +92,6 @@ function InternalAlertsPage() { error: 0, snoozed: 0, }); - const { hasAnyData, isAllRequestsComplete } = useHasData(); const [esQuery, setEsQuery] = useState<{ bool: BoolQuery }>(); const timeBuckets = useTimeBuckets(); const bucketSize = useMemo( @@ -173,10 +170,6 @@ function InternalAlertsPage() { const manageRulesHref = http.basePath.prepend('/app/observability/alerts/rules'); - if (!hasAnyData && !isAllRequestsComplete) { - return ; - } - return ( ; - } - return userCasesPermissions.read ? ( diff --git a/x-pack/plugins/observability/public/pages/slos/slos.tsx b/x-pack/plugins/observability/public/pages/slos/slos.tsx index 579b1bb8d19ae..e010df224ce1c 100644 --- a/x-pack/plugins/observability/public/pages/slos/slos.tsx +++ b/x-pack/plugins/observability/public/pages/slos/slos.tsx @@ -32,7 +32,7 @@ export function SlosPage() { const { hasWriteCapabilities } = useCapabilities(); const { hasAtLeast } = useLicense(); - const { isInitialLoading, isLoading, isError, data: sloList } = useFetchSloList(); + const { isLoading, isError, data: sloList } = useFetchSloList(); const { total } = sloList ?? { total: 0 }; const { storeAutoRefreshState, getAutoRefreshState } = useAutoRefreshStorage(); @@ -63,10 +63,6 @@ export function SlosPage() { storeAutoRefreshState(!isAutoRefreshing); }; - if (isInitialLoading) { - return null; - } - return ( component was not working here // so I've recreated this simple version for this purpose. @@ -65,7 +66,11 @@ export const routes = { }, [LANDING_PATH]: { handler: () => { - return ; + return ( + + + + ); }, params: {}, exact: true, @@ -73,9 +78,11 @@ export const routes = { [OVERVIEW_PATH]: { handler: () => { return ( - - - + + + + + ); }, params: {}, From 025fb3031b61005390534f5180394b30d65552a5 Mon Sep 17 00:00:00 2001 From: Faisal Kanout Date: Tue, 28 Nov 2023 12:03:37 +0300 Subject: [PATCH 02/14] [BUG][OBX-UX-MNGMT] Fix IS_NOT_BETWEEN comparator for the custom threshold, Infra, Metric rules (#171925) ## Summary Fixes https://github.com/elastic/kibana/issues/169524 Fix the painless script that evaluates the IS_NOT_BETWEEN for the Custom Threshold, Metric, Infra rules. Screenshot 2023-11-24 at 15 03 18 --------- --- .../lib/create_condition_script.ts | 3 ++- .../metric_threshold/lib/create_condition_script.ts | 3 ++- .../custom_threshold/lib/create_condition_script.ts | 3 ++- .../custom_threshold_rule/documents_count_fired.ts | 10 +++++----- .../custom_threshold_rule/documents_count_fired.ts | 8 ++++---- 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_condition_script.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_condition_script.ts index 37a39d215eddd..a62f5d92dac06 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_condition_script.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_condition_script.ts @@ -25,7 +25,8 @@ export const createConditionScript = ( } if (comparator === Comparator.OUTSIDE_RANGE && threshold.length === 2) { return { - source: `params.value < params.threshold0 && params.value > params.threshold1 ? 1 : 0`, + // OUTSIDE_RANGE/NOT BETWEEN is the opposite of BETWEEN. Use the BETWEEN condition and switch the 1 and 0 + source: `params.value > params.threshold0 && params.value < params.threshold1 ? 0 : 1`, params: { threshold0: threshold[0], threshold1: threshold[1], diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/create_condition_script.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/create_condition_script.ts index b4285863dbccb..1320607685a87 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/create_condition_script.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/create_condition_script.ts @@ -18,7 +18,8 @@ export const createConditionScript = (threshold: number[], comparator: Comparato } if (comparator === Comparator.OUTSIDE_RANGE && threshold.length === 2) { return { - source: `params.value < params.threshold0 && params.value > params.threshold1 ? 1 : 0`, + // OUTSIDE_RANGE/NOT BETWEEN is the opposite of BETWEEN. Use the BETWEEN condition and switch the 1 and 0 + source: `params.value > params.threshold0 && params.value < params.threshold1 ? 0 : 1`, params: { threshold0: threshold[0], threshold1: threshold[1], diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/create_condition_script.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/create_condition_script.ts index ad4aaa980aa63..2e5eda9fa32b4 100644 --- a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/create_condition_script.ts +++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/create_condition_script.ts @@ -19,7 +19,8 @@ export const createConditionScript = (threshold: number[], comparator: Comparato } if (comparator === Comparator.OUTSIDE_RANGE && threshold.length === 2) { return { - source: `params.value < params.threshold0 && params.value > params.threshold1 ? 1 : 0`, + // OUTSIDE_RANGE/NOT BETWEEN is the opposite of BETWEEN. Use the BETWEEN condition and switch the 1 and 0 + source: `params.value > params.threshold0 && params.value < params.threshold1 ? 0 : 1`, params: { threshold0: threshold[0], threshold1: threshold[1], diff --git a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/documents_count_fired.ts b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/documents_count_fired.ts index a602dc9012850..a1057e4ed6f74 100644 --- a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/documents_count_fired.ts +++ b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/documents_count_fired.ts @@ -91,8 +91,8 @@ export default function ({ getService }: FtrProviderContext) { params: { criteria: [ { - comparator: Comparator.GT, - threshold: [2], + comparator: Comparator.OUTSIDE_RANGE, + threshold: [1, 2], timeSize: 1, timeUnit: 'm', metrics: [{ name: 'A', filter: '', aggType: Aggregators.COUNT }], @@ -186,8 +186,8 @@ export default function ({ getService }: FtrProviderContext) { .eql({ criteria: [ { - comparator: '>', - threshold: [2], + comparator: Comparator.OUTSIDE_RANGE, + threshold: [1, 2], timeSize: 1, timeUnit: 'm', metrics: [{ name: 'A', filter: '', aggType: 'count' }], @@ -211,7 +211,7 @@ export default function ({ getService }: FtrProviderContext) { `https://localhost:5601/app/observability/alerts?_a=(kuery:%27kibana.alert.uuid:%20%22${alertId}%22%27%2CrangeFrom:%27${rangeFrom}%27%2CrangeTo:now%2Cstatus:all)` ); expect(resp.hits.hits[0]._source?.reason).eql( - `Document count is 3, above the threshold of 2. (duration: 1 min, data view: ${DATE_VIEW_NAME})` + `Document count is 3, not between the threshold of 1 and 2. (duration: 1 min, data view: ${DATE_VIEW_NAME})` ); expect(resp.hits.hits[0]._source?.value).eql('3'); }); diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/documents_count_fired.ts b/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/documents_count_fired.ts index 01f04b12f0c50..453da41b81196 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/documents_count_fired.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/documents_count_fired.ts @@ -84,8 +84,8 @@ export default function ({ getService }: FtrProviderContext) { params: { criteria: [ { - comparator: Comparator.GT, - threshold: [2], + comparator: Comparator.OUTSIDE_RANGE, + threshold: [1, 2], timeSize: 1, timeUnit: 'm', metrics: [{ name: 'A', filter: '', aggType: Aggregators.COUNT }], @@ -176,8 +176,8 @@ export default function ({ getService }: FtrProviderContext) { .eql({ criteria: [ { - comparator: '>', - threshold: [2], + comparator: Comparator.OUTSIDE_RANGE, + threshold: [1, 2], timeSize: 1, timeUnit: 'm', metrics: [{ name: 'A', filter: '', aggType: 'count' }], From df5383edf2d9fbed64e40aeed222060e2f552cdf Mon Sep 17 00:00:00 2001 From: Vadim Kibana <82822460+vadimkibana@users.noreply.github.com> Date: Tue, 28 Nov 2023 10:20:30 +0100 Subject: [PATCH 03/14] [Reporting] Prevent server from crashing due to concurrent deletes (#171858) ## Summary Closes https://github.com/elastic/kibana/issues/171363 - The thing that crashes the server is `refresh: 'wait_for'`. - I've also changed `await promisify` in one place, as that looked risky to me. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../server/routes/common/jobs/get_job_routes.ts | 10 +++++----- .../reporting/server/routes/common/jobs/jobs_query.ts | 6 +----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/reporting/server/routes/common/jobs/get_job_routes.ts b/x-pack/plugins/reporting/server/routes/common/jobs/get_job_routes.ts index ddd226187adf0..a88abae999be0 100644 --- a/x-pack/plugins/reporting/server/routes/common/jobs/get_job_routes.ts +++ b/x-pack/plugins/reporting/server/routes/common/jobs/get_job_routes.ts @@ -5,12 +5,9 @@ * 2.0. */ -import { promisify } from 'util'; - import { schema, TypeOf } from '@kbn/config-schema'; import { KibanaRequest, KibanaResponseFactory } from '@kbn/core-http-server'; import { ALLOWED_JOB_CONTENT_TYPES } from '@kbn/reporting-common'; - import { getCounters } from '..'; import { ReportingCore } from '../../..'; import { getContentStream } from '../../../lib'; @@ -84,9 +81,12 @@ export const commonJobsRouteHandlerFactory = (reporting: ReportingCore) => { return jobManagementPreRouting(reporting, res, docId, user, counters, async (doc) => { const docIndex = doc.index; const stream = await getContentStream(reporting, { id: docId, index: docIndex }); - /** @note Overwriting existing content with an empty buffer to remove all the chunks. */ - await promisify(stream.end.bind(stream, '', 'utf8'))(); + await new Promise((resolve) => { + stream.end('', 'utf8', () => { + resolve(); + }); + }); await jobsQuery.delete(docIndex, docId); return res.ok({ diff --git a/x-pack/plugins/reporting/server/routes/common/jobs/jobs_query.ts b/x-pack/plugins/reporting/server/routes/common/jobs/jobs_query.ts index 9efe74a0c3aac..b87c6040e46b4 100644 --- a/x-pack/plugins/reporting/server/routes/common/jobs/jobs_query.ts +++ b/x-pack/plugins/reporting/server/routes/common/jobs/jobs_query.ts @@ -206,11 +206,7 @@ export function jobsQueryFactory(reportingCore: ReportingCore): JobsQueryFactory async delete(deleteIndex, id) { try { const { asInternalUser: elasticsearchClient } = await reportingCore.getEsClient(); - - // Using `wait_for` helps avoid users seeing recently-deleted reports temporarily flashing back in the - // job listing. - const query = { id, index: deleteIndex, refresh: 'wait_for' as const }; - + const query = { id, index: deleteIndex }; return await elasticsearchClient.delete(query, { meta: true }); } catch (error) { throw new Error( From fcdd44ffeb159beb99346e467b7daa2fc52c1d33 Mon Sep 17 00:00:00 2001 From: Vadim Kibana <82822460+vadimkibana@users.noreply.github.com> Date: Tue, 28 Nov 2023 10:21:37 +0100 Subject: [PATCH 04/14] [Reporting] Do not stretch report delete button (#171862) ## Summary Closes https://github.com/elastic/kibana/issues/171853 Now there is no extra spacing around the button: image --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../reporting/public/management/report_listing_table.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/reporting/public/management/report_listing_table.tsx b/x-pack/plugins/reporting/public/management/report_listing_table.tsx index 61b9e7d541928..c8dcbf77aba30 100644 --- a/x-pack/plugins/reporting/public/management/report_listing_table.tsx +++ b/x-pack/plugins/reporting/public/management/report_listing_table.tsx @@ -397,12 +397,12 @@ export class ReportListingTable extends Component { return ( {this.state.selectedJobs.length > 0 && ( - +
{this.renderDeleteButton()} - +
)} Date: Tue, 28 Nov 2023 12:24:45 +0200 Subject: [PATCH 05/14] [Cases] Add new sub feature privilege to prevent access to the cases settings page (#170635) --- .../features/src/app_features_keys.ts | 1 + .../features/src/cases/kibana_sub_features.ts | 36 ++++++++ .../plugins/cases/common/constants/index.ts | 1 + x-pack/plugins/cases/common/index.ts | 2 + x-pack/plugins/cases/common/ui/types.ts | 8 +- .../cases/common/utils/capabilities.test.tsx | 34 +++++++ .../cases/common/utils/capabilities.ts | 3 + .../public/client/helpers/can_use_cases.ts | 14 ++- .../client/helpers/capabilities.test.ts | 52 ++++++++++- .../public/client/helpers/capabilities.ts | 6 +- .../cases/public/common/lib/kibana/hooks.ts | 2 + .../common/lib/kibana/kibana_react.mock.tsx | 1 + .../cases/public/common/mock/permissions.ts | 11 ++- .../components/all_cases/header.test.tsx | 4 +- .../components/all_cases/nav_buttons.test.tsx | 55 ++++++++++++ .../components/all_cases/nav_buttons.tsx | 4 +- .../public/components/app/routes.test.tsx | 10 +-- .../cases/public/components/app/routes.tsx | 2 +- .../callout/callout.test.tsx | 90 +++++++------------ .../use_push_to_service/callout/callout.tsx | 5 +- x-pack/plugins/cases/public/mocks.ts | 2 + x-pack/plugins/cases/server/features.ts | 27 ++++++ .../action_menu/action_menu.test.tsx | 9 -- .../exploratory_view.test.tsx | 9 -- .../header/add_to_case_action.test.tsx | 21 +++-- .../header/add_to_case_action.tsx | 3 +- .../common/feature_kibana_privileges.ts | 10 +++ .../__snapshots__/oss_features.test.ts.snap | 6 ++ .../feature_privilege_iterator.test.ts | 26 ++++++ .../feature_privilege_iterator.ts | 4 + .../plugins/features/server/feature_schema.ts | 1 + .../hooks/use_get_user_cases_permissions.tsx | 50 ----------- .../alert_details/alert_details.test.tsx | 10 --- .../pages/alert_details/alert_details.tsx | 2 +- .../alerts/components/alert_actions.test.tsx | 21 ++++- .../pages/alerts/components/alert_actions.tsx | 7 +- .../public/pages/cases/cases.tsx | 6 +- .../pages/cases/components/cases.stories.tsx | 2 + .../public/utils/cases_permissions.ts | 15 ---- x-pack/plugins/observability/server/plugin.ts | 30 +++++++ .../hooks/use_get_user_cases_permissions.tsx | 52 ----------- .../observability_shared/public/index.ts | 3 +- .../public/utils/cases_permissions.ts | 12 +++ .../__snapshots__/cases.test.ts.snap | 13 ++- .../feature_privilege_builder/cases.test.ts | 18 ++-- .../feature_privilege_builder/cases.ts | 16 +++- .../security_solution/public/app/routes.tsx | 4 +- .../security_solution/public/cases/links.ts | 4 +- .../public/cases/pages/index.tsx | 4 +- .../public/cases_test_utils.ts | 29 +++--- .../event_details/event_details.test.tsx | 8 +- .../event_details/insights/insights.test.tsx | 10 +-- .../event_details/insights/insights.tsx | 6 +- .../insights/related_cases.test.tsx | 9 +- .../components/events_viewer/index.test.tsx | 8 -- .../components/sessions_viewer/index.test.tsx | 9 -- .../visualization_actions/actions.test.tsx | 1 + .../use_add_to_existing_case.test.tsx | 10 +-- .../use_add_to_existing_case.tsx | 5 +- .../use_add_to_new_case.test.tsx | 10 +-- .../use_add_to_new_case.tsx | 6 +- .../common/lib/kibana/__mocks__/index.ts | 1 - .../public/common/lib/kibana/hooks.ts | 45 +--------- .../common/lib/kibana/kibana_react.mock.ts | 2 +- .../alert_context_menu.test.tsx | 23 +++-- .../use_add_to_case_actions.test.tsx | 13 +-- .../use_add_to_case_actions.tsx | 5 +- .../rule_preview/preview_histogram.test.tsx | 6 -- .../take_action_dropdown/index.test.tsx | 11 ++- .../hooks/use_show_related_cases.test.tsx | 27 +++++- .../shared/hooks/use_show_related_cases.ts | 7 +- .../components/recent_cases/index.tsx | 4 +- .../components/sidebar/sidebar.test.tsx | 6 +- .../overview/components/sidebar/sidebar.tsx | 12 ++- .../overview/pages/data_quality.test.tsx | 28 ++++-- .../public/overview/pages/data_quality.tsx | 18 ++-- .../pages/detection_response.test.tsx | 27 ++++-- .../overview/pages/detection_response.tsx | 7 +- .../flyout/action_menu/index.test.tsx | 44 ++++++--- .../components/flyout/action_menu/index.tsx | 7 +- .../flyout/add_to_case_button/index.test.tsx | 9 +- .../flyout/add_to_case_button/index.tsx | 4 +- .../event_details/flyout/footer.test.tsx | 12 +-- .../side_panel/event_details/index.test.tsx | 12 +-- .../components/timeline/index.test.tsx | 7 -- .../apis/security/privileges.ts | 27 +++++- .../apis/security/privileges_basic.ts | 27 +++++- .../security_solution/server/plugin.ts | 27 +++++- .../plugins/cases/public/application.tsx | 1 + 89 files changed, 757 insertions(+), 501 deletions(-) create mode 100644 x-pack/plugins/cases/common/utils/capabilities.test.tsx create mode 100644 x-pack/plugins/cases/public/components/all_cases/nav_buttons.test.tsx delete mode 100644 x-pack/plugins/observability/public/hooks/use_get_user_cases_permissions.tsx delete mode 100644 x-pack/plugins/observability/public/utils/cases_permissions.ts delete mode 100644 x-pack/plugins/observability_shared/public/hooks/use_get_user_cases_permissions.tsx diff --git a/x-pack/packages/security-solution/features/src/app_features_keys.ts b/x-pack/packages/security-solution/features/src/app_features_keys.ts index ab54c64cf8992..ed8923cdb229a 100644 --- a/x-pack/packages/security-solution/features/src/app_features_keys.ts +++ b/x-pack/packages/security-solution/features/src/app_features_keys.ts @@ -99,6 +99,7 @@ export enum SecuritySubFeatureId { /** Sub-features IDs for Cases */ export enum CasesSubFeatureId { deleteCases = 'deleteCasesSubFeature', + casesSettings = 'casesSettingsSubFeature', } /** Sub-features IDs for Security Assistant */ diff --git a/x-pack/packages/security-solution/features/src/cases/kibana_sub_features.ts b/x-pack/packages/security-solution/features/src/cases/kibana_sub_features.ts index 3cbdb3f0e9123..1f49d01f979da 100644 --- a/x-pack/packages/security-solution/features/src/cases/kibana_sub_features.ts +++ b/x-pack/packages/security-solution/features/src/cases/kibana_sub_features.ts @@ -17,6 +17,7 @@ import type { CasesFeatureParams } from './types'; */ export const getCasesBaseKibanaSubFeatureIds = (): CasesSubFeatureId[] => [ CasesSubFeatureId.deleteCases, + CasesSubFeatureId.casesSettings, ]; /** @@ -60,7 +61,42 @@ export const getCasesSubFeaturesMap = ({ ], }; + const casesSettingsCasesSubFeature: SubFeatureConfig = { + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.casesSettingsSubFeatureName', + { + defaultMessage: 'Case Settings', + } + ), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'cases_settings', + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.casesSettingsSubFeatureDetails', + { + defaultMessage: 'Edit Case Settings', + } + ), + includeIn: 'all', + savedObject: { + all: [...savedObjects.files], + read: [...savedObjects.files], + }, + cases: { + settings: [APP_ID], + }, + ui: uiCapabilities.settings, + }, + ], + }, + ], + }; + return new Map([ [CasesSubFeatureId.deleteCases, deleteCasesSubFeature], + [CasesSubFeatureId.casesSettings, casesSettingsCasesSubFeature], ]); }; diff --git a/x-pack/plugins/cases/common/constants/index.ts b/x-pack/plugins/cases/common/constants/index.ts index 5a540b610135c..d4b6839e1dc37 100644 --- a/x-pack/plugins/cases/common/constants/index.ts +++ b/x-pack/plugins/cases/common/constants/index.ts @@ -162,6 +162,7 @@ export const READ_CASES_CAPABILITY = 'read_cases' as const; export const UPDATE_CASES_CAPABILITY = 'update_cases' as const; export const DELETE_CASES_CAPABILITY = 'delete_cases' as const; export const PUSH_CASES_CAPABILITY = 'push_cases' as const; +export const CASES_SETTINGS_CAPABILITY = 'cases_settings' as const; export const CASES_CONNECTORS_CAPABILITY = 'cases_connectors' as const; /** diff --git a/x-pack/plugins/cases/common/index.ts b/x-pack/plugins/cases/common/index.ts index 4283adf4c081a..520b5ca079b63 100644 --- a/x-pack/plugins/cases/common/index.ts +++ b/x-pack/plugins/cases/common/index.ts @@ -29,6 +29,7 @@ export type { Ecs, CaseViewRefreshPropInterface, CasesPermissions, + CasesCapabilities, CasesStatus, } from './ui/types'; @@ -52,6 +53,7 @@ export { CASE_COMMENT_SAVED_OBJECT, CASES_CONNECTORS_CAPABILITY, GET_CONNECTORS_CONFIGURE_API_TAG, + CASES_SETTINGS_CAPABILITY, } from './constants'; export type { AttachmentAttributes } from './types/domain'; diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index 2a76e56a59fe0..01d006a0dcd7d 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -12,7 +12,11 @@ import type { READ_CASES_CAPABILITY, UPDATE_CASES_CAPABILITY, } from '..'; -import type { CASES_CONNECTORS_CAPABILITY, PUSH_CASES_CAPABILITY } from '../constants'; +import type { + CASES_CONNECTORS_CAPABILITY, + CASES_SETTINGS_CAPABILITY, + PUSH_CASES_CAPABILITY, +} from '../constants'; import type { SnakeToCamelCase } from '../types'; import type { CaseSeverity, @@ -299,6 +303,7 @@ export interface CasesPermissions { delete: boolean; push: boolean; connectors: boolean; + settings: boolean; } export interface CasesCapabilities { @@ -308,4 +313,5 @@ export interface CasesCapabilities { [DELETE_CASES_CAPABILITY]: boolean; [PUSH_CASES_CAPABILITY]: boolean; [CASES_CONNECTORS_CAPABILITY]: boolean; + [CASES_SETTINGS_CAPABILITY]: boolean; } diff --git a/x-pack/plugins/cases/common/utils/capabilities.test.tsx b/x-pack/plugins/cases/common/utils/capabilities.test.tsx new file mode 100644 index 0000000000000..07b82ea0d0e8f --- /dev/null +++ b/x-pack/plugins/cases/common/utils/capabilities.test.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createUICapabilities } from './capabilities'; + +describe('createUICapabilities', () => { + it('returns the UI capabilities correctly', () => { + expect(createUICapabilities()).toMatchInlineSnapshot(` + Object { + "all": Array [ + "create_cases", + "read_cases", + "update_cases", + "push_cases", + "cases_connectors", + ], + "delete": Array [ + "delete_cases", + ], + "read": Array [ + "read_cases", + "cases_connectors", + ], + "settings": Array [ + "cases_settings", + ], + } + `); + }); +}); diff --git a/x-pack/plugins/cases/common/utils/capabilities.ts b/x-pack/plugins/cases/common/utils/capabilities.ts index 28b3fa00f9272..6b33dd8c8dceb 100644 --- a/x-pack/plugins/cases/common/utils/capabilities.ts +++ b/x-pack/plugins/cases/common/utils/capabilities.ts @@ -12,12 +12,14 @@ import { PUSH_CASES_CAPABILITY, READ_CASES_CAPABILITY, UPDATE_CASES_CAPABILITY, + CASES_SETTINGS_CAPABILITY, } from '../constants'; export interface CasesUiCapabilities { all: readonly string[]; read: readonly string[]; delete: readonly string[]; + settings: readonly string[]; } /** * Return the UI capabilities for each type of operation. These strings must match the values defined in the UI @@ -33,4 +35,5 @@ export const createUICapabilities = (): CasesUiCapabilities => ({ ] as const, read: [READ_CASES_CAPABILITY, CASES_CONNECTORS_CAPABILITY] as const, delete: [DELETE_CASES_CAPABILITY] as const, + settings: [CASES_SETTINGS_CAPABILITY] as const, }); diff --git a/x-pack/plugins/cases/public/client/helpers/can_use_cases.ts b/x-pack/plugins/cases/public/client/helpers/can_use_cases.ts index 1cc22c0799702..90b0d3b18908f 100644 --- a/x-pack/plugins/cases/public/client/helpers/can_use_cases.ts +++ b/x-pack/plugins/cases/public/client/helpers/can_use_cases.ts @@ -40,10 +40,19 @@ export const canUseCases = acc.update = acc.update || userCapabilitiesForOwner.update; acc.delete = acc.delete || userCapabilitiesForOwner.delete; acc.push = acc.push || userCapabilitiesForOwner.push; + acc.connectors = acc.connectors || userCapabilitiesForOwner.connectors; + acc.settings = acc.settings || userCapabilitiesForOwner.settings; + const allFromAcc = - acc.create && acc.read && acc.update && acc.delete && acc.push && acc.connectors; + acc.create && + acc.read && + acc.update && + acc.delete && + acc.push && + acc.connectors && + acc.settings; + acc.all = acc.all || userCapabilitiesForOwner.all || allFromAcc; - acc.connectors = acc.connectors || userCapabilitiesForOwner.connectors; return acc; }, @@ -55,6 +64,7 @@ export const canUseCases = delete: false, push: false, connectors: false, + settings: false, } ); diff --git a/x-pack/plugins/cases/public/client/helpers/capabilities.test.ts b/x-pack/plugins/cases/public/client/helpers/capabilities.test.ts index a3f741f373032..ce374243b10b2 100644 --- a/x-pack/plugins/cases/public/client/helpers/capabilities.test.ts +++ b/x-pack/plugins/cases/public/client/helpers/capabilities.test.ts @@ -17,6 +17,7 @@ describe('getUICapabilities', () => { "delete": false, "push": false, "read": false, + "settings": false, "update": false, } `); @@ -31,6 +32,7 @@ describe('getUICapabilities', () => { "delete": false, "push": false, "read": false, + "settings": false, "update": false, } `); @@ -45,6 +47,7 @@ describe('getUICapabilities', () => { "delete": false, "push": false, "read": false, + "settings": false, "update": false, } `); @@ -68,6 +71,7 @@ describe('getUICapabilities', () => { "delete": false, "push": false, "read": false, + "settings": false, "update": false, } `); @@ -82,6 +86,7 @@ describe('getUICapabilities', () => { "delete": false, "push": false, "read": false, + "settings": false, "update": false, } `); @@ -105,6 +110,7 @@ describe('getUICapabilities', () => { "delete": true, "push": true, "read": true, + "settings": false, "update": true, } `); @@ -113,23 +119,65 @@ describe('getUICapabilities', () => { it('returns false for the all field when cases_connectors is false', () => { expect( getUICapabilities({ - create_cases: false, + create_cases: true, read_cases: true, update_cases: true, delete_cases: true, push_cases: true, cases_connectors: false, + cases_settings: true, }) ).toMatchInlineSnapshot(` Object { "all": false, "connectors": false, - "create": false, + "create": true, + "delete": true, + "push": true, + "read": true, + "settings": true, + "update": true, + } + `); + }); + + it('returns false for the all field when cases_settings is false', () => { + expect( + getUICapabilities({ + create_cases: true, + read_cases: true, + update_cases: true, + delete_cases: true, + push_cases: true, + cases_connectors: true, + cases_settings: false, + }) + ).toMatchInlineSnapshot(` + Object { + "all": false, + "connectors": true, + "create": true, "delete": true, "push": true, "read": true, + "settings": false, "update": true, } `); }); + + it('returns true for cases_settings when it is set to true in the ui capabilities', () => { + expect(getUICapabilities({ cases_settings: true })).toMatchInlineSnapshot(` + Object { + "all": false, + "connectors": false, + "create": false, + "delete": false, + "push": false, + "read": false, + "settings": true, + "update": false, + } + `); + }); }); diff --git a/x-pack/plugins/cases/public/client/helpers/capabilities.ts b/x-pack/plugins/cases/public/client/helpers/capabilities.ts index 278512fef623c..9be5b5f05f646 100644 --- a/x-pack/plugins/cases/public/client/helpers/capabilities.ts +++ b/x-pack/plugins/cases/public/client/helpers/capabilities.ts @@ -8,6 +8,7 @@ import type { CasesPermissions } from '../../../common'; import { CASES_CONNECTORS_CAPABILITY, + CASES_SETTINGS_CAPABILITY, CREATE_CASES_CAPABILITY, DELETE_CASES_CAPABILITY, PUSH_CASES_CAPABILITY, @@ -24,7 +25,9 @@ export const getUICapabilities = ( const deletePriv = !!featureCapabilities?.[DELETE_CASES_CAPABILITY]; const push = !!featureCapabilities?.[PUSH_CASES_CAPABILITY]; const connectors = !!featureCapabilities?.[CASES_CONNECTORS_CAPABILITY]; - const all = create && read && update && deletePriv && push && connectors; + const settings = !!featureCapabilities?.[CASES_SETTINGS_CAPABILITY]; + + const all = create && read && update && deletePriv && push && connectors && settings; return { all, @@ -34,5 +37,6 @@ export const getUICapabilities = ( delete: deletePriv, push, connectors, + settings, }; }; diff --git a/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts b/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts index fdb5a22e66985..39b4d3d1edc76 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts +++ b/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts @@ -194,6 +194,7 @@ export const useApplicationCapabilities = (): UseApplicationCapabilities => { delete: permissions.delete, push: permissions.push, connectors: permissions.connectors, + settings: permissions.settings, }, visualize: { crud: !!capabilities.visualize?.save, read: !!capabilities.visualize?.show }, dashboard: { @@ -215,6 +216,7 @@ export const useApplicationCapabilities = (): UseApplicationCapabilities => { permissions.delete, permissions.push, permissions.connectors, + permissions.settings, ] ); }; diff --git a/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.tsx b/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.tsx index 31ea452874c28..195c1f433a8e7 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.tsx +++ b/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.tsx @@ -75,6 +75,7 @@ export const createStartServicesMock = ({ license }: StartServiceArgs = {}): Sta delete_cases: true, push_cases: true, cases_connectors: true, + cases_settings: true, }, visualize: { save: true, show: true }, dashboard: { show: true, createNew: true }, diff --git a/x-pack/plugins/cases/public/common/mock/permissions.ts b/x-pack/plugins/cases/public/common/mock/permissions.ts index 4d68e9d36c776..fce274cd7f338 100644 --- a/x-pack/plugins/cases/public/common/mock/permissions.ts +++ b/x-pack/plugins/cases/public/common/mock/permissions.ts @@ -16,7 +16,9 @@ export const noCasesPermissions = () => delete: false, push: false, connectors: false, + settings: false, }); + export const readCasesPermissions = () => buildCasesPermissions({ read: true, @@ -25,6 +27,7 @@ export const readCasesPermissions = () => delete: false, push: false, connectors: true, + settings: false, }); export const noCreateCasesPermissions = () => buildCasesPermissions({ create: false }); export const noUpdateCasesPermissions = () => buildCasesPermissions({ update: false }); @@ -34,6 +37,7 @@ export const writeCasesPermissions = () => buildCasesPermissions({ read: false } export const onlyDeleteCasesPermission = () => buildCasesPermissions({ read: false, create: false, update: false, delete: true, push: false }); export const noConnectorsCasePermission = () => buildCasesPermissions({ connectors: false }); +export const noCasesSettingsPermission = () => buildCasesPermissions({ settings: false }); export const buildCasesPermissions = (overrides: Partial> = {}) => { const create = overrides.create ?? true; @@ -42,7 +46,8 @@ export const buildCasesPermissions = (overrides: Partial delete_cases: false, push_cases: false, cases_connectors: false, + cases_settings: false, }); export const readCasesCapabilities = () => buildCasesCapabilities({ @@ -71,6 +78,7 @@ export const readCasesCapabilities = () => update_cases: false, delete_cases: false, push_cases: false, + cases_settings: false, }); export const writeCasesCapabilities = () => { return buildCasesCapabilities({ @@ -86,5 +94,6 @@ export const buildCasesCapabilities = (overrides?: Partial) = delete_cases: overrides?.delete_cases ?? true, push_cases: overrides?.push_cases ?? true, cases_connectors: overrides?.cases_connectors ?? true, + cases_settings: overrides?.cases_settings ?? true, }; }; diff --git a/x-pack/plugins/cases/public/components/all_cases/header.test.tsx b/x-pack/plugins/cases/public/components/all_cases/header.test.tsx index 08bd228b32e11..333f330394442 100644 --- a/x-pack/plugins/cases/public/components/all_cases/header.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/header.test.tsx @@ -46,9 +46,9 @@ describe('CasesTableHeader', () => { expect(result.getByTestId('configure-case-button')).toBeInTheDocument(); }); - it('does not display the configure button when the user does not have update privileges', () => { + it('does not display the configure button when the user does not have settings privileges', () => { appMockRender = createAppMockRenderer({ - permissions: buildCasesPermissions({ update: false }), + permissions: buildCasesPermissions({ settings: false }), }); const result = appMockRender.render(); diff --git a/x-pack/plugins/cases/public/components/all_cases/nav_buttons.test.tsx b/x-pack/plugins/cases/public/components/all_cases/nav_buttons.test.tsx new file mode 100644 index 0000000000000..b825f5c27f2eb --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/nav_buttons.test.tsx @@ -0,0 +1,55 @@ +/* + * 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 React from 'react'; +import { screen } from '@testing-library/react'; +import type { AppMockRenderer } from '../../common/mock'; +import { + createAppMockRenderer, + noCasesSettingsPermission, + noCreateCasesPermissions, + buildCasesPermissions, +} from '../../common/mock'; +import { NavButtons } from './nav_buttons'; + +describe('NavButtons', () => { + let appMockRenderer: AppMockRenderer; + + beforeEach(() => { + appMockRenderer = createAppMockRenderer(); + }); + + it('shows the configure case button', () => { + appMockRenderer.render(); + + expect(screen.getByTestId('configure-case-button')).toBeInTheDocument(); + }); + + it('does not render the case create button with no create permissions', () => { + appMockRenderer = createAppMockRenderer({ permissions: noCreateCasesPermissions() }); + appMockRenderer.render(); + + expect(screen.queryByTestId('createNewCaseBtn')).not.toBeInTheDocument(); + }); + + it('does not render the case configure button with no settings permissions', () => { + appMockRenderer = createAppMockRenderer({ permissions: noCasesSettingsPermission() }); + appMockRenderer.render(); + + expect(screen.queryByTestId('configure-case-button')).not.toBeInTheDocument(); + }); + + it('does not render any button with no create and no settings permissions', () => { + appMockRenderer = createAppMockRenderer({ + permissions: buildCasesPermissions({ create: false, settings: false }), + }); + appMockRenderer.render(); + + expect(screen.queryByTestId('createNewCaseBtn')).not.toBeInTheDocument(); + expect(screen.queryByTestId('configure-case-button')).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/all_cases/nav_buttons.tsx b/x-pack/plugins/cases/public/components/all_cases/nav_buttons.tsx index aafb287eed869..05febf94f431f 100644 --- a/x-pack/plugins/cases/public/components/all_cases/nav_buttons.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/nav_buttons.tsx @@ -43,14 +43,14 @@ export const NavButtons: FunctionComponent = ({ actionsErrors }) => { [navigateToCreateCase] ); - if (!permissions.create && !permissions.update) { + if (!permissions.create && !permissions.settings) { return null; } return ( - {permissions.update && ( + {permissions.settings && ( { }); }); - describe('Configure cases', () => { - it('navigates to the configure cases page', () => { + describe('Cases settings', () => { + it('navigates to the cases settings page', () => { renderWithRouter(['/cases/configure']); expect(screen.getByText('Settings')).toBeInTheDocument(); }); - it('shows the no privileges page if the user does not have update privileges', () => { - renderWithRouter(['/cases/configure'], noUpdateCasesPermissions()); + it('shows the no privileges page if the user does not have settings privileges', () => { + renderWithRouter(['/cases/configure'], noCasesSettingsPermission()); expect(screen.getByText('Privileges required')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/cases/public/components/app/routes.tsx b/x-pack/plugins/cases/public/components/app/routes.tsx index 7f4e35fc4ac81..27bf536b11ab8 100644 --- a/x-pack/plugins/cases/public/components/app/routes.tsx +++ b/x-pack/plugins/cases/public/components/app/routes.tsx @@ -71,7 +71,7 @@ const CasesRoutesComponent: React.FC = ({ - {permissions.update ? ( + {permissions.settings ? ( ) : ( diff --git a/x-pack/plugins/cases/public/components/use_push_to_service/callout/callout.test.tsx b/x-pack/plugins/cases/public/components/use_push_to_service/callout/callout.test.tsx index 868e9be03ff6f..2dfd0d188f5bb 100644 --- a/x-pack/plugins/cases/public/components/use_push_to_service/callout/callout.test.tsx +++ b/x-pack/plugins/cases/public/components/use_push_to_service/callout/callout.test.tsx @@ -6,14 +6,18 @@ */ import React from 'react'; -import { mount } from 'enzyme'; +import { screen } from '@testing-library/react'; import type { CallOutProps } from './callout'; import { CallOut } from './callout'; import { CLOSED_CASE_PUSH_ERROR_ID } from './types'; -import { TestProviders } from '../../../common/mock'; +import type { AppMockRenderer } from '../../../common/mock'; +import { noCasesSettingsPermission, createAppMockRenderer } from '../../../common/mock'; +import userEvent from '@testing-library/user-event'; describe('Callout', () => { + let appMockRenderer: AppMockRenderer; + const handleButtonClick = jest.fn(); const defaultProps: CallOutProps = { id: 'md5-hex', @@ -31,50 +35,19 @@ describe('Callout', () => { beforeEach(() => { jest.clearAllMocks(); + appMockRenderer = createAppMockRenderer(); }); it('It renders the callout', () => { - const wrapper = mount(); - expect(wrapper.find(`[data-test-subj="case-callout-md5-hex"]`).exists()).toBeTruthy(); - expect(wrapper.find(`[data-test-subj="callout-messages-md5-hex"]`).exists()).toBeTruthy(); - expect(wrapper.find(`[data-test-subj="callout-onclick-md5-hex"]`).exists()).toBeTruthy(); + appMockRenderer.render(); + expect(screen.getByTestId('case-callout-md5-hex')).toBeInTheDocument(); + expect(screen.getByTestId('callout-messages-md5-hex')).toBeInTheDocument(); + expect(screen.getByTestId('callout-onclick-md5-hex')).toBeInTheDocument(); }); it('does not shows any messages when the list is empty', () => { - const wrapper = mount(); - expect(wrapper.find(`[data-test-subj="callout-messages-md5-hex"]`).exists()).toBeFalsy(); - }); - - it('transform the button color correctly - primary', () => { - const wrapper = mount(); - const className = - wrapper.find(`button[data-test-subj="callout-onclick-md5-hex"]`).first().prop('className') ?? - ''; - expect(className.includes('primary')).toBeTruthy(); - }); - - it('transform the button color correctly - success', () => { - const wrapper = mount(); - const className = - wrapper.find(`button[data-test-subj="callout-onclick-md5-hex"]`).first().prop('className') ?? - ''; - expect(className.includes('success')).toBeTruthy(); - }); - - it('transform the button color correctly - warning', () => { - const wrapper = mount(); - const className = - wrapper.find(`button[data-test-subj="callout-onclick-md5-hex"]`).first().prop('className') ?? - ''; - expect(className.includes('warning')).toBeTruthy(); - }); - - it('transform the button color correctly - danger', () => { - const wrapper = mount(); - const className = - wrapper.find(`button[data-test-subj="callout-onclick-md5-hex"]`).first().prop('className') ?? - ''; - expect(className.includes('danger')).toBeTruthy(); + appMockRenderer.render(); + expect(screen.queryByTestId('callout-messages-md5-hex')).not.toBeInTheDocument(); }); it('does not show the button when case is closed error is present', () => { @@ -89,15 +62,9 @@ describe('Callout', () => { ], }; - const wrapper = mount( - - - - ); + appMockRenderer.render(); - expect(wrapper.find(`button[data-test-subj="callout-onclick-md5-hex"]`).exists()).toEqual( - false - ); + expect(screen.queryByTestId('callout-onclick-md5-hex')).not.toBeInTheDocument(); }); it('does not show the button when license error is present', () => { @@ -106,22 +73,27 @@ describe('Callout', () => { hasLicenseError: true, }; - const wrapper = mount( - - - - ); + appMockRenderer.render(); + + expect(screen.queryByTestId('callout-onclick-md5-hex')).not.toBeInTheDocument(); + }); + + it('does not show the button with no settings permissions', () => { + appMockRenderer = createAppMockRenderer({ permissions: noCasesSettingsPermission() }); - expect(wrapper.find(`button[data-test-subj="callout-onclick-md5-hex"]`).exists()).toEqual( - false - ); + appMockRenderer.render(); + + expect(screen.queryByTestId('callout-onclick-md5-hex')).not.toBeInTheDocument(); }); // use this for storage if we ever want to bring that back it('onClick passes id and type', () => { - const wrapper = mount(); - expect(wrapper.find(`[data-test-subj="callout-onclick-md5-hex"]`).exists()).toBeTruthy(); - wrapper.find(`button[data-test-subj="callout-onclick-md5-hex"]`).simulate('click'); + appMockRenderer.render(); + + expect(screen.getByTestId('callout-onclick-md5-hex')).toBeInTheDocument(); + + userEvent.click(screen.getByTestId('callout-onclick-md5-hex')); + expect(handleButtonClick.mock.calls[0][1]).toEqual('md5-hex'); expect(handleButtonClick.mock.calls[0][2]).toEqual('primary'); }); diff --git a/x-pack/plugins/cases/public/components/use_push_to_service/callout/callout.tsx b/x-pack/plugins/cases/public/components/use_push_to_service/callout/callout.tsx index ffd19f8366252..c94fbb826df48 100644 --- a/x-pack/plugins/cases/public/components/use_push_to_service/callout/callout.tsx +++ b/x-pack/plugins/cases/public/components/use_push_to_service/callout/callout.tsx @@ -12,6 +12,7 @@ import React, { memo, useCallback, useMemo } from 'react'; import type { ErrorMessage } from './types'; import { CLOSED_CASE_PUSH_ERROR_ID } from './types'; import * as i18n from './translations'; +import { useCasesContext } from '../../cases_context/use_cases_context'; export interface CallOutProps { handleButtonClick: ( @@ -32,6 +33,8 @@ const CallOutComponent = ({ type, hasLicenseError, }: CallOutProps) => { + const { permissions } = useCasesContext(); + const handleCallOut = useCallback( (e) => handleButtonClick(e, id, type), [handleButtonClick, id, type] @@ -57,7 +60,7 @@ const CallOutComponent = ({ size="s" > - {!isCaseClosed && !hasLicenseError && ( + {!isCaseClosed && !hasLicenseError && permissions.settings && ( = { update: false, delete: false, push: false, + connectors: false, + settings: false, }), getRuleIdFromEvent: jest.fn(), groupAlertsByRule: jest.fn(), diff --git a/x-pack/plugins/cases/server/features.ts b/x-pack/plugins/cases/server/features.ts index b44c3589ecd08..62276ad4fcc30 100644 --- a/x-pack/plugins/cases/server/features.ts +++ b/x-pack/plugins/cases/server/features.ts @@ -100,6 +100,33 @@ export const getCasesKibanaFeature = (): KibanaFeatureConfig => { }, ], }, + { + name: i18n.translate('xpack.cases.features.casesSettingsSubFeatureName', { + defaultMessage: 'Case Settings', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'cases_settings', + name: i18n.translate('xpack.cases.features.casesSettingsSubFeatureDetails', { + defaultMessage: 'Edit Case Settings', + }), + includeIn: 'all', + savedObject: { + all: [...filesSavedObjectTypes], + read: [...filesSavedObjectTypes], + }, + cases: { + settings: [APP_ID], + }, + ui: capabilities.settings, + }, + ], + }, + ], + }, ], }; }; diff --git a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx index 4e81ca840199f..5d43f5bd1e6df 100644 --- a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx +++ b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx @@ -12,8 +12,6 @@ import { sampleAttribute } from '../../configurations/test_data/sample_attribute import * as pluginHook from '../../../../../hooks/use_plugin_context'; import { TypedLensByValueInput } from '@kbn/lens-plugin/public'; import { ExpViewActionMenuContent } from './action_menu'; -import { noCasesPermissions as mockUseGetCasesPermissions } from '@kbn/observability-shared-plugin/public'; -import * as obsHooks from '@kbn/observability-shared-plugin/public/hooks/use_get_user_cases_permissions'; jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({ appMountParameters: { @@ -21,13 +19,6 @@ jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({ }, } as any); -jest.spyOn(obsHooks, 'useGetUserCasesPermissions').mockImplementation( - () => - ({ - useGetUserCasesPermissions: jest.fn(() => mockUseGetCasesPermissions()), - } as any) -); - describe('Action Menu', function () { afterAll(() => { jest.clearAllMocks(); diff --git a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/exploratory_view.test.tsx b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/exploratory_view.test.tsx index 7b4e0cb5cc57f..83cae3e8b4ebb 100644 --- a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/exploratory_view.test.tsx +++ b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/exploratory_view.test.tsx @@ -12,8 +12,6 @@ import { ExploratoryView } from './exploratory_view'; import * as obsvDataViews from '../../../utils/observability_data_views/observability_data_views'; import * as pluginHook from '../../../hooks/use_plugin_context'; import { createStubIndexPattern } from '@kbn/data-plugin/common/stubs'; -import { noCasesPermissions as mockUseGetCasesPermissions } from '@kbn/observability-shared-plugin/public/utils/cases_permissions'; -import * as obsHooks from '@kbn/observability-shared-plugin/public/hooks/use_get_user_cases_permissions'; jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({ appMountParameters: { @@ -21,13 +19,6 @@ jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({ }, } as any); -jest.spyOn(obsHooks, 'useGetUserCasesPermissions').mockImplementation( - () => - ({ - useGetUserCasesPermissions: jest.fn(() => mockUseGetCasesPermissions()), - } as any) -); - describe('ExploratoryView', () => { mockAppDataView(); diff --git a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx index ffccfdf6db3f2..1d42716bf405d 100644 --- a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx +++ b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx @@ -13,10 +13,15 @@ import * as useCaseHook from '../hooks/use_add_to_case'; import * as datePicker from '../components/date_range_picker'; import moment from 'moment'; import { noCasesPermissions as mockUseGetCasesPermissions } from '@kbn/observability-shared-plugin/public'; -import * as obsHooks from '@kbn/observability-shared-plugin/public/hooks/use_get_user_cases_permissions'; -jest.spyOn(obsHooks, 'useGetUserCasesPermissions').mockReturnValue(mockUseGetCasesPermissions()); describe('AddToCaseAction', function () { + const coreRenderProps = { + cases: { + ui: { getAllCasesSelectorModal: jest.fn() }, + helpers: { canUseCases: () => mockUseGetCasesPermissions() }, + }, + }; + beforeEach(() => { jest.spyOn(datePicker, 'parseRelativeDate').mockRestore(); }); @@ -26,7 +31,8 @@ describe('AddToCaseAction', function () { + />, + { core: coreRenderProps } ); expect(await findByText('Add to case')).toBeInTheDocument(); }); @@ -39,7 +45,8 @@ describe('AddToCaseAction', function () { + />, + { core: coreRenderProps } ); expect(await findByText('Add to case')).toBeInTheDocument(); @@ -60,7 +67,8 @@ describe('AddToCaseAction', function () { const useAddToCaseHook = jest.spyOn(useCaseHook, 'useAddToCase'); const { getByText } = render( - + , + { core: coreRenderProps } ); expect(await forNearestButton(getByText)('Add to case')).toBeDisabled(); @@ -95,7 +103,7 @@ describe('AddToCaseAction', function () { lensAttributes={{ title: 'Performance distribution' } as any} timeRange={{ to: 'now', from: 'now-5m' }} />, - { initSeries } + { initSeries, core: coreRenderProps } ); fireEvent.click(await findByText('Add to case')); @@ -111,6 +119,7 @@ describe('AddToCaseAction', function () { delete: false, push: false, connectors: false, + settings: false, }, }) ); diff --git a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.tsx b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.tsx index 1cbb904f6500d..590451eaea6ef 100644 --- a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.tsx +++ b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.tsx @@ -16,7 +16,6 @@ import { } from '@kbn/cases-plugin/public'; import { TypedLensByValueInput } from '@kbn/lens-plugin/public'; import { observabilityFeatureId } from '@kbn/observability-shared-plugin/public'; -import { useGetUserCasesPermissions } from '@kbn/observability-shared-plugin/public'; import { ObservabilityAppServices } from '../../../../application/types'; import { useAddToCase } from '../hooks/use_add_to_case'; import { parseRelativeDate } from '../components/date_range_picker'; @@ -37,7 +36,7 @@ export function AddToCaseAction({ timeRange, }: AddToCaseProps) { const kServices = useKibana().services; - const userCasesPermissions = useGetUserCasesPermissions(); + const userCasesPermissions = kServices.cases.helpers.canUseCases([observabilityFeatureId]); const { cases, diff --git a/x-pack/plugins/features/common/feature_kibana_privileges.ts b/x-pack/plugins/features/common/feature_kibana_privileges.ts index 3a56b080bb91f..49c001c890b69 100644 --- a/x-pack/plugins/features/common/feature_kibana_privileges.ts +++ b/x-pack/plugins/features/common/feature_kibana_privileges.ts @@ -204,6 +204,16 @@ export interface FeatureKibanaPrivileges { * ``` */ delete?: readonly string[]; + /** + * List of case owners which users should have settings access to when granted this privilege. + * @example + * ```ts + * { + * settings: ['securitySolution'] + * } + * ``` + */ + settings?: readonly string[]; }; /** diff --git a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap index 62e868e77e520..7247c1d23cb0c 100644 --- a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap +++ b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap @@ -558,6 +558,7 @@ Array [ "delete": Array [], "push": Array [], "read": Array [], + "settings": Array [], "update": Array [], }, "catalogue": Array [ @@ -710,6 +711,7 @@ Array [ "delete": Array [], "push": Array [], "read": Array [], + "settings": Array [], "update": Array [], }, "catalogue": Array [ @@ -1032,6 +1034,7 @@ Array [ "delete": Array [], "push": Array [], "read": Array [], + "settings": Array [], "update": Array [], }, "catalogue": Array [ @@ -1169,6 +1172,7 @@ Array [ "delete": Array [], "push": Array [], "read": Array [], + "settings": Array [], "update": Array [], }, "catalogue": Array [ @@ -1321,6 +1325,7 @@ Array [ "delete": Array [], "push": Array [], "read": Array [], + "settings": Array [], "update": Array [], }, "catalogue": Array [ @@ -1643,6 +1648,7 @@ Array [ "delete": Array [], "push": Array [], "read": Array [], + "settings": Array [], "update": Array [], }, "catalogue": Array [ diff --git a/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.test.ts b/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.test.ts index f2f6ed1071f81..58a39c85bf9e9 100644 --- a/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.test.ts +++ b/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.test.ts @@ -77,6 +77,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type'], delete: ['cases-delete-type'], push: ['cases-push-type'], + settings: ['cases-settings-type'], }, ui: ['ui-action'], }, @@ -146,6 +147,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type'], delete: ['cases-delete-type'], push: ['cases-push-type'], + settings: ['cases-settings-type'], }, ui: ['ui-action'], }, @@ -214,6 +216,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type'], delete: ['cases-delete-type'], push: ['cases-push-type'], + settings: ['cases-settings-type'], }, ui: ['ui-action'], }, @@ -284,6 +287,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type'], delete: ['cases-delete-type'], push: ['cases-push-type'], + settings: ['cases-settings-type'], }, ui: ['ui-action'], }, @@ -324,6 +328,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type'], delete: ['cases-delete-type'], push: ['cases-push-type'], + settings: ['cases-settings-type'], }, ui: ['ui-action'], }, @@ -385,6 +390,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-sub-type'], delete: ['cases-delete-sub-type'], push: ['cases-push-sub-type'], + settings: ['cases-settings-sub-type'], }, ui: ['ui-sub-type'], }, @@ -431,6 +437,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type'], delete: ['cases-delete-type'], push: ['cases-push-type'], + settings: ['cases-settings-type'], }, ui: ['ui-action'], }, @@ -498,6 +505,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type'], delete: ['cases-delete-type'], push: ['cases-push-type'], + settings: ['cases-settings-type'], }, ui: ['ui-action'], }, @@ -559,6 +567,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-sub-type'], delete: ['cases-delete-sub-type'], push: ['cases-push-sub-type'], + settings: ['cases-settings-sub-type'], }, ui: ['ui-sub-type'], }, @@ -605,6 +614,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type'], delete: ['cases-delete-type'], push: ['cases-push-type'], + settings: ['cases-settings-type'], }, ui: ['ui-action'], }, @@ -672,6 +682,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type'], delete: ['cases-delete-type'], push: ['cases-push-type'], + settings: ['cases-settings-type'], }, ui: ['ui-action'], }, @@ -734,6 +745,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-sub-type'], delete: ['cases-delete-sub-type'], push: ['cases-push-sub-type'], + settings: ['cases-settings-sub-type'], }, ui: ['ui-sub-type'], }, @@ -783,6 +795,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type', 'cases-update-sub-type'], delete: ['cases-delete-type', 'cases-delete-sub-type'], push: ['cases-push-type', 'cases-push-sub-type'], + settings: ['cases-settings-type', 'cases-settings-sub-type'], }, ui: ['ui-action', 'ui-sub-type'], }, @@ -818,6 +831,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-sub-type'], delete: ['cases-delete-sub-type'], push: ['cases-push-sub-type'], + settings: ['cases-settings-sub-type'], }, ui: ['ui-action', 'ui-sub-type'], }, @@ -860,6 +874,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type'], delete: ['cases-delete-type'], push: ['cases-push-type'], + settings: ['cases-settings-type'], }, ui: ['ui-action'], }, @@ -964,6 +979,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type'], delete: ['cases-delete-type'], push: ['cases-push-type'], + settings: ['cases-settings-type'], }, ui: ['ui-action'], }, @@ -998,6 +1014,7 @@ describe('featurePrivilegeIterator', () => { update: [], delete: [], push: [], + settings: [], }, ui: ['ui-action'], }, @@ -1038,6 +1055,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type'], delete: ['cases-delete-type'], push: ['cases-push-type'], + settings: ['cases-settings-type'], }, ui: ['ui-action'], }, @@ -1100,6 +1118,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-sub-type'], delete: ['cases-delete-sub-type'], push: ['cases-push-sub-type'], + settings: ['cases-settings-sub-type'], }, ui: ['ui-sub-type'], }, @@ -1149,6 +1168,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type', 'cases-update-sub-type'], delete: ['cases-delete-type', 'cases-delete-sub-type'], push: ['cases-push-type', 'cases-push-sub-type'], + settings: ['cases-settings-type', 'cases-settings-sub-type'], }, ui: ['ui-action', 'ui-sub-type'], }, @@ -1341,6 +1361,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-sub-type'], delete: ['cases-delete-sub-type'], push: ['cases-push-sub-type'], + settings: ['cases-settings-sub-type'], }, ui: ['ui-sub-type'], }, @@ -1390,6 +1411,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-sub-type'], delete: ['cases-delete-sub-type'], push: ['cases-push-sub-type'], + settings: ['cases-settings-sub-type'], }, ui: ['ui-sub-type'], }, @@ -1425,6 +1447,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-sub-type'], delete: ['cases-delete-sub-type'], push: ['cases-push-sub-type'], + settings: ['cases-settings-sub-type'], }, ui: ['ui-sub-type'], }, @@ -1465,6 +1488,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type'], delete: ['cases-delete-type'], push: ['cases-push-type'], + settings: ['cases-settings-type'], }, ui: ['ui-action'], }, @@ -1555,6 +1579,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type'], delete: ['cases-delete-type'], push: ['cases-push-type'], + settings: ['cases-settings-type'], }, ui: ['ui-action'], }, @@ -1589,6 +1614,7 @@ describe('featurePrivilegeIterator', () => { update: [], delete: [], push: [], + settings: [], }, ui: ['ui-action'], }, diff --git a/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.ts b/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.ts index 9392b3b3fee33..0d1dc8e3ab788 100644 --- a/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.ts +++ b/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.ts @@ -147,6 +147,10 @@ function mergeWithSubFeatures( subFeaturePrivilege.cases?.delete ?? [] ), push: mergeArrays(mergedConfig.cases?.push ?? [], subFeaturePrivilege.cases?.push ?? []), + settings: mergeArrays( + mergedConfig.cases?.settings ?? [], + subFeaturePrivilege.cases?.settings ?? [] + ), }; } return mergedConfig; diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts index 416a9bf534b3a..b332ea355dcc0 100644 --- a/x-pack/plugins/features/server/feature_schema.ts +++ b/x-pack/plugins/features/server/feature_schema.ts @@ -82,6 +82,7 @@ const casesSchemaObject = schema.maybe( update: schema.maybe(casesSchema), delete: schema.maybe(casesSchema), push: schema.maybe(casesSchema), + settings: schema.maybe(casesSchema), }) ); diff --git a/x-pack/plugins/observability/public/hooks/use_get_user_cases_permissions.tsx b/x-pack/plugins/observability/public/hooks/use_get_user_cases_permissions.tsx deleted file mode 100644 index ea80fc8f8cc1c..0000000000000 --- a/x-pack/plugins/observability/public/hooks/use_get_user_cases_permissions.tsx +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 { useEffect, useState } from 'react'; -import { CasesPermissions } from '@kbn/cases-plugin/common'; -import { useKibana } from '../utils/kibana_react'; -import { casesFeatureId } from '../../common'; - -export function useGetUserCasesPermissions() { - const [casesPermissions, setCasesPermissions] = useState({ - all: false, - read: false, - create: false, - update: false, - delete: false, - push: false, - connectors: false, - }); - const uiCapabilities = useKibana().services.application.capabilities; - - const casesCapabilities = useKibana().services.cases.helpers.getUICapabilities( - uiCapabilities[casesFeatureId] - ); - - useEffect(() => { - setCasesPermissions({ - all: casesCapabilities.all, - create: casesCapabilities.create, - read: casesCapabilities.read, - update: casesCapabilities.update, - delete: casesCapabilities.delete, - push: casesCapabilities.push, - connectors: casesCapabilities.connectors, - }); - }, [ - casesCapabilities.all, - casesCapabilities.create, - casesCapabilities.read, - casesCapabilities.update, - casesCapabilities.delete, - casesCapabilities.push, - casesCapabilities.connectors, - ]); - - return casesPermissions; -} diff --git a/x-pack/plugins/observability/public/pages/alert_details/alert_details.test.tsx b/x-pack/plugins/observability/public/pages/alert_details/alert_details.test.tsx index dfb1f571b6d43..756f90ecd97cf 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/alert_details.test.tsx +++ b/x-pack/plugins/observability/public/pages/alert_details/alert_details.test.tsx @@ -76,16 +76,6 @@ jest.mock('../../hooks/use_fetch_rule', () => { }; }); jest.mock('@kbn/observability-shared-plugin/public'); -jest.mock('../../hooks/use_get_user_cases_permissions', () => ({ - useGetUserCasesPermissions: () => ({ - all: true, - create: true, - delete: true, - push: true, - read: true, - update: true, - }), -})); const useFetchAlertDetailMock = useFetchAlertDetail as jest.Mock; const useParamsMock = useParams as jest.Mock; diff --git a/x-pack/plugins/observability/public/pages/alert_details/alert_details.tsx b/x-pack/plugins/observability/public/pages/alert_details/alert_details.tsx index a7661ba43fddb..06da477896c5d 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/alert_details.tsx +++ b/x-pack/plugins/observability/public/pages/alert_details/alert_details.tsx @@ -58,7 +58,7 @@ export function AlertDetails() { const [isLoading, alert] = useFetchAlertDetail(alertId); const [ruleTypeModel, setRuleTypeModel] = useState(null); const CasesContext = getCasesContext(); - const userCasesPermissions = canUseCases(); + const userCasesPermissions = canUseCases([observabilityFeatureId]); const { rule } = useFetchRule({ ruleId: alert?.fields[ALERT_RULE_UUID], }); diff --git a/x-pack/plugins/observability/public/pages/alerts/components/alert_actions.test.tsx b/x-pack/plugins/observability/public/pages/alerts/components/alert_actions.test.tsx index 70dfcd0a82d43..ec87151c44be1 100644 --- a/x-pack/plugins/observability/public/pages/alerts/components/alert_actions.test.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/components/alert_actions.test.tsx @@ -18,6 +18,7 @@ import * as pluginContext from '../../../hooks/use_plugin_context'; import { ConfigSchema, ObservabilityPublicPluginsStart } from '../../../plugin'; import { AppMountParameters, CoreStart } from '@kbn/core/public'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; +import { allCasesPermissions, noCasesPermissions } from '@kbn/observability-shared-plugin/public'; const refresh = jest.fn(); const caseHooksReturnedValue = { @@ -36,15 +37,13 @@ mockUseKibanaReturnValue.services.cases.hooks.useCasesAddToExistingCaseModal.moc caseHooksReturnedValue ); +mockUseKibanaReturnValue.services.cases.helpers.canUseCases.mockReturnValue(allCasesPermissions()); + jest.mock('../../../utils/kibana_react', () => ({ __esModule: true, useKibana: jest.fn(() => mockUseKibanaReturnValue), })); -jest.mock('../../../hooks/use_get_user_cases_permissions', () => ({ - useGetUserCasesPermissions: jest.fn(() => ({ create: true, read: true })), -})); - jest.mock('@kbn/triggers-actions-ui-plugin/public/common/lib/kibana/kibana_react', () => ({ useKibana: jest.fn(() => ({ services: { notifications: { toasts: { addDanger: jest.fn(), addSuccess: jest.fn() } } }, @@ -175,4 +174,18 @@ describe('ObservabilityActions component', () => { expect(refresh).toHaveBeenCalled(); }); + + it('should hide the case actions without permissions', async () => { + mockUseKibanaReturnValue.services.cases.helpers.canUseCases.mockReturnValue( + noCasesPermissions() + ); + + const wrapper = await setup('nothing'); + wrapper.find('[data-test-subj="alertsTableRowActionMore"]').hostNodes().simulate('click'); + + expect(wrapper.find('[data-test-subj="add-to-new-case-action"]').hostNodes().length).toBe(0); + expect(wrapper.find('[data-test-subj="add-to-existing-case-action"]').hostNodes().length).toBe( + 0 + ); + }); }); diff --git a/x-pack/plugins/observability/public/pages/alerts/components/alert_actions.tsx b/x-pack/plugins/observability/public/pages/alerts/components/alert_actions.tsx index 7799655908907..9a3bce0a9a326 100644 --- a/x-pack/plugins/observability/public/pages/alerts/components/alert_actions.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/components/alert_actions.tsx @@ -30,12 +30,11 @@ import { } from '@kbn/rule-data-utils'; import { useBulkUntrackAlerts } from '@kbn/triggers-actions-ui-plugin/public'; import { useKibana } from '../../../utils/kibana_react'; -import { useGetUserCasesPermissions } from '../../../hooks/use_get_user_cases_permissions'; import { isAlertDetailsEnabledPerApp } from '../../../utils/is_alert_details_enabled'; import { parseAlert } from '../helpers/parse_alert'; import { paths } from '../../../../common/locators/paths'; import { RULE_DETAILS_PAGE_ID } from '../../rule_details/constants'; -import type { ObservabilityRuleTypeRegistry } from '../../..'; +import { observabilityFeatureId, ObservabilityRuleTypeRegistry } from '../../..'; import type { ConfigSchema } from '../../../plugin'; import type { TopAlert } from '../../../typings/alerts'; @@ -62,15 +61,15 @@ export function AlertActions({ }: Props) { const { cases: { - helpers: { getRuleIdFromEvent }, + helpers: { getRuleIdFromEvent, canUseCases }, hooks: { useCasesAddToNewCaseFlyout, useCasesAddToExistingCaseModal }, }, http: { basePath: { prepend }, }, } = useKibana().services; - const userCasesPermissions = useGetUserCasesPermissions(); const { mutateAsync: untrackAlerts } = useBulkUntrackAlerts(); + const userCasesPermissions = canUseCases([observabilityFeatureId]); const parseObservabilityAlert = useMemo( () => parseAlert(observabilityRuleTypeRegistry), diff --git a/x-pack/plugins/observability/public/pages/cases/cases.tsx b/x-pack/plugins/observability/public/pages/cases/cases.tsx index 2cb31d98414fc..13fc73e909601 100644 --- a/x-pack/plugins/observability/public/pages/cases/cases.tsx +++ b/x-pack/plugins/observability/public/pages/cases/cases.tsx @@ -7,15 +7,17 @@ import React from 'react'; -import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions'; +import { observabilityFeatureId } from '../../../common'; import { usePluginContext } from '../../hooks/use_plugin_context'; import { Cases } from './components/cases'; import { CaseFeatureNoPermissions } from './components/feature_no_permissions'; import { HeaderMenu } from '../overview/components/header_menu/header_menu'; +import { useKibana } from '../../utils/kibana_react'; export function CasesPage() { - const userCasesPermissions = useGetUserCasesPermissions(); const { ObservabilityPageTemplate } = usePluginContext(); + const { canUseCases } = useKibana().services.cases.helpers; + const userCasesPermissions = canUseCases([observabilityFeatureId]); return userCasesPermissions.read ? ( diff --git a/x-pack/plugins/observability/public/pages/cases/components/cases.stories.tsx b/x-pack/plugins/observability/public/pages/cases/components/cases.stories.tsx index d0fc1d01734f2..695165d05e1bd 100644 --- a/x-pack/plugins/observability/public/pages/cases/components/cases.stories.tsx +++ b/x-pack/plugins/observability/public/pages/cases/components/cases.stories.tsx @@ -27,6 +27,7 @@ const defaultProps: CasesProps = { push: true, update: true, connectors: true, + settings: true, }, }; @@ -43,5 +44,6 @@ CasesPageWithNoPermissions.args = { push: false, update: false, connectors: false, + settings: false, }, }; diff --git a/x-pack/plugins/observability/public/utils/cases_permissions.ts b/x-pack/plugins/observability/public/utils/cases_permissions.ts deleted file mode 100644 index 2b3ff9cfbaf54..0000000000000 --- a/x-pack/plugins/observability/public/utils/cases_permissions.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const noCasesPermissions = () => ({ - all: false, - create: false, - read: false, - update: false, - delete: false, - push: false, -}); diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index 17476d335f48f..d1ca0e45b74a2 100644 --- a/x-pack/plugins/observability/server/plugin.ts +++ b/x-pack/plugins/observability/server/plugin.ts @@ -175,6 +175,36 @@ export class ObservabilityPlugin implements Plugin { }, ], }, + { + name: i18n.translate('xpack.observability.featureRegistry.casesSettingsSubFeatureName', { + defaultMessage: 'Case Settings', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'cases_settings', + name: i18n.translate( + 'xpack.observability.featureRegistry.casesSettingsSubFeatureDetails', + { + defaultMessage: 'Edit Case Settings', + } + ), + includeIn: 'all', + savedObject: { + all: [...filesSavedObjectTypes], + read: [...filesSavedObjectTypes], + }, + cases: { + settings: [observabilityFeatureId], + }, + ui: casesCapabilities.settings, + }, + ], + }, + ], + }, ], }); diff --git a/x-pack/plugins/observability_shared/public/hooks/use_get_user_cases_permissions.tsx b/x-pack/plugins/observability_shared/public/hooks/use_get_user_cases_permissions.tsx deleted file mode 100644 index 21c6a08815b76..0000000000000 --- a/x-pack/plugins/observability_shared/public/hooks/use_get_user_cases_permissions.tsx +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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 { useEffect, useState } from 'react'; -import { CasesPermissions } from '@kbn/cases-plugin/common'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { casesFeatureId } from '../../common'; -import { ObservabilitySharedStart } from '../plugin'; - -export function useGetUserCasesPermissions() { - const [casesPermissions, setCasesPermissions] = useState({ - all: false, - read: false, - create: false, - update: false, - delete: false, - push: false, - connectors: false, - }); - const uiCapabilities = useKibana().services.application!.capabilities; - - const casesCapabilities = - useKibana().services.cases.helpers.getUICapabilities( - uiCapabilities[casesFeatureId] - ); - - useEffect(() => { - setCasesPermissions({ - all: casesCapabilities.all, - create: casesCapabilities.create, - read: casesCapabilities.read, - update: casesCapabilities.update, - delete: casesCapabilities.delete, - push: casesCapabilities.push, - connectors: casesCapabilities.connectors, - }); - }, [ - casesCapabilities.all, - casesCapabilities.create, - casesCapabilities.read, - casesCapabilities.update, - casesCapabilities.delete, - casesCapabilities.push, - casesCapabilities.connectors, - ]); - - return casesPermissions; -} diff --git a/x-pack/plugins/observability_shared/public/index.ts b/x-pack/plugins/observability_shared/public/index.ts index cbb59c3d2debe..8d8556e509e25 100644 --- a/x-pack/plugins/observability_shared/public/index.ts +++ b/x-pack/plugins/observability_shared/public/index.ts @@ -57,7 +57,6 @@ export { } from './hooks/use_track_metric'; export type { TrackEvent } from './hooks/use_track_metric'; export { useQuickTimeRanges } from './hooks/use_quick_time_ranges'; -export { useGetUserCasesPermissions } from './hooks/use_get_user_cases_permissions'; export { useTimeZone } from './hooks/use_time_zone'; export { useChartTheme } from './hooks/use_chart_theme'; export { useLinkProps, shouldHandleLinkEvent } from './hooks/use_link_props'; @@ -66,7 +65,7 @@ export { NavigationWarningPromptProvider, Prompt } from './components/navigation export type { ApmIndicesConfig, UXMetrics } from './types'; -export { noCasesPermissions } from './utils/cases_permissions'; +export { noCasesPermissions, allCasesPermissions } from './utils/cases_permissions'; export { type ObservabilityActionContextMenuItemProps, diff --git a/x-pack/plugins/observability_shared/public/utils/cases_permissions.ts b/x-pack/plugins/observability_shared/public/utils/cases_permissions.ts index a0b6a8aed95b0..0ceea46ad0d38 100644 --- a/x-pack/plugins/observability_shared/public/utils/cases_permissions.ts +++ b/x-pack/plugins/observability_shared/public/utils/cases_permissions.ts @@ -13,4 +13,16 @@ export const noCasesPermissions = () => ({ delete: false, push: false, connectors: false, + settings: false, +}); + +export const allCasesPermissions = () => ({ + all: true, + create: true, + read: true, + update: true, + delete: true, + push: true, + connectors: true, + settings: true, }); diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/__snapshots__/cases.test.ts.snap b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/__snapshots__/cases.test.ts.snap index fc31927e6cfb5..1874a17515e19 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/__snapshots__/cases.test.ts.snap +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/__snapshots__/cases.test.ts.snap @@ -5,7 +5,6 @@ Array [ "cases:observability/pushCase", "cases:observability/createCase", "cases:observability/createComment", - "cases:observability/createConfiguration", "cases:observability/getCase", "cases:observability/getComment", "cases:observability/getTags", @@ -14,9 +13,10 @@ Array [ "cases:observability/findConfigurations", "cases:observability/updateCase", "cases:observability/updateComment", - "cases:observability/updateConfiguration", "cases:observability/deleteCase", "cases:observability/deleteComment", + "cases:observability/createConfiguration", + "cases:observability/updateConfiguration", ] `; @@ -24,7 +24,6 @@ exports[`cases feature_privilege_builder within feature grants create privileges Array [ "cases:securitySolution/createCase", "cases:securitySolution/createComment", - "cases:securitySolution/createConfiguration", ] `; @@ -52,10 +51,16 @@ Array [ ] `; +exports[`cases feature_privilege_builder within feature grants settings privileges under feature with id observability 1`] = ` +Array [ + "cases:observability/createConfiguration", + "cases:observability/updateConfiguration", +] +`; + exports[`cases feature_privilege_builder within feature grants update privileges under feature with id observability 1`] = ` Array [ "cases:observability/updateCase", "cases:observability/updateComment", - "cases:observability/updateConfiguration", ] `; diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts index d4d49a5334f1d..ad0563ef7a827 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts @@ -47,6 +47,7 @@ describe(`cases`, () => { ['read', 'observability'], ['update', 'observability'], ['delete', 'securitySolution'], + ['settings', 'observability'], ])('grants %s privileges under feature with id %s', (operation, featureID) => { const actions = new Actions(); const casesFeaturePrivilege = new FeaturePrivilegeCasesBuilder(actions); @@ -55,7 +56,6 @@ describe(`cases`, () => { cases: { [operation]: [featureID], }, - savedObject: { all: [], read: [], @@ -88,8 +88,8 @@ describe(`cases`, () => { update: ['obs'], delete: ['security'], read: ['obs'], + settings: ['security'], }, - savedObject: { all: [], read: [], @@ -113,7 +113,6 @@ describe(`cases`, () => { "cases:security/pushCase", "cases:security/createCase", "cases:security/createComment", - "cases:security/createConfiguration", "cases:security/getCase", "cases:security/getComment", "cases:security/getTags", @@ -122,9 +121,10 @@ describe(`cases`, () => { "cases:security/findConfigurations", "cases:security/updateCase", "cases:security/updateComment", - "cases:security/updateConfiguration", "cases:security/deleteCase", "cases:security/deleteComment", + "cases:security/createConfiguration", + "cases:security/updateConfiguration", "cases:obs/getCase", "cases:obs/getComment", "cases:obs/getTags", @@ -133,7 +133,6 @@ describe(`cases`, () => { "cases:obs/findConfigurations", "cases:obs/updateCase", "cases:obs/updateComment", - "cases:obs/updateConfiguration", ] `); }); @@ -147,7 +146,6 @@ describe(`cases`, () => { all: ['security', 'other-security'], read: ['obs', 'other-obs'], }, - savedObject: { all: [], read: [], @@ -171,7 +169,6 @@ describe(`cases`, () => { "cases:security/pushCase", "cases:security/createCase", "cases:security/createComment", - "cases:security/createConfiguration", "cases:security/getCase", "cases:security/getComment", "cases:security/getTags", @@ -180,13 +177,13 @@ describe(`cases`, () => { "cases:security/findConfigurations", "cases:security/updateCase", "cases:security/updateComment", - "cases:security/updateConfiguration", "cases:security/deleteCase", "cases:security/deleteComment", + "cases:security/createConfiguration", + "cases:security/updateConfiguration", "cases:other-security/pushCase", "cases:other-security/createCase", "cases:other-security/createComment", - "cases:other-security/createConfiguration", "cases:other-security/getCase", "cases:other-security/getComment", "cases:other-security/getTags", @@ -195,9 +192,10 @@ describe(`cases`, () => { "cases:other-security/findConfigurations", "cases:other-security/updateCase", "cases:other-security/updateComment", - "cases:other-security/updateConfiguration", "cases:other-security/deleteCase", "cases:other-security/deleteComment", + "cases:other-security/createConfiguration", + "cases:other-security/updateConfiguration", "cases:obs/getCase", "cases:obs/getComment", "cases:obs/getTags", diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts index 0f442c9d871e1..b54ba77777dd8 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts @@ -13,11 +13,16 @@ import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; export type CasesSupportedOperations = typeof allOperations[number]; -// if you add a value here you'll likely also need to make changes here: -// x-pack/plugins/cases/server/authorization/index.ts +/** + * If you add a new operation type (all, push, update, etc) you should also + * extend the mapping here x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.ts + * + * Also if you add a new operation (createCase, updateCase, etc) here you'll likely also need to make changes here: + * x-pack/plugins/cases/server/authorization/index.ts + */ const pushOperations = ['pushCase'] as const; -const createOperations = ['createCase', 'createComment', 'createConfiguration'] as const; +const createOperations = ['createCase', 'createComment'] as const; const readOperations = [ 'getCase', 'getComment', @@ -26,14 +31,16 @@ const readOperations = [ 'getUserActions', 'findConfigurations', ] as const; -const updateOperations = ['updateCase', 'updateComment', 'updateConfiguration'] as const; +const updateOperations = ['updateCase', 'updateComment'] as const; const deleteOperations = ['deleteCase', 'deleteComment'] as const; +const settingsOperations = ['createConfiguration', 'updateConfiguration'] as const; const allOperations = [ ...pushOperations, ...createOperations, ...readOperations, ...updateOperations, ...deleteOperations, + ...settingsOperations, ] as const; export class FeaturePrivilegeCasesBuilder extends BaseFeaturePrivilegeBuilder { @@ -57,6 +64,7 @@ export class FeaturePrivilegeCasesBuilder extends BaseFeaturePrivilegeBuilder { ...getCasesPrivilege(readOperations, privilegeDefinition.cases?.read), ...getCasesPrivilege(updateOperations, privilegeDefinition.cases?.update), ...getCasesPrivilege(deleteOperations, privilegeDefinition.cases?.delete), + ...getCasesPrivilege(settingsOperations, privilegeDefinition.cases?.settings), ]); } } diff --git a/x-pack/plugins/security_solution/public/app/routes.tsx b/x-pack/plugins/security_solution/public/app/routes.tsx index 73fe2615b0e5a..307d8f4c32376 100644 --- a/x-pack/plugins/security_solution/public/app/routes.tsx +++ b/x-pack/plugins/security_solution/public/app/routes.tsx @@ -14,7 +14,7 @@ import type { AppLeaveHandler } from '@kbn/core/public'; import { APP_ID } from '../../common/constants'; import { RouteCapture } from '../common/components/endpoint/route_capture'; -import { useGetUserCasesPermissions, useKibana } from '../common/lib/kibana'; +import { useKibana } from '../common/lib/kibana'; import type { AppAction } from '../common/store/actions'; import { ManageRoutesSpy } from '../common/utils/route/manage_spy_routes'; import { NotFoundPage } from './404'; @@ -29,7 +29,7 @@ interface RouterProps { const PageRouterComponent: FC = ({ children, history, onAppLeave }) => { const { cases } = useKibana().services; const CasesContext = cases.ui.getCasesContext(); - const userCasesPermissions = useGetUserCasesPermissions(); + const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); const dispatch = useDispatch<(action: AppAction) => void>(); useEffect(() => { return () => { diff --git a/x-pack/plugins/security_solution/public/cases/links.ts b/x-pack/plugins/security_solution/public/cases/links.ts index 3017fa28816e3..2d2f6d94b351a 100644 --- a/x-pack/plugins/security_solution/public/cases/links.ts +++ b/x-pack/plugins/security_solution/public/cases/links.ts @@ -8,7 +8,7 @@ import { CREATE_CASES_CAPABILITY, READ_CASES_CAPABILITY, - UPDATE_CASES_CAPABILITY, + CASES_SETTINGS_CAPABILITY, } from '@kbn/cases-plugin/common'; import { getCasesDeepLinks } from '@kbn/cases-plugin/public'; import { CASES_FEATURE_ID, CASES_PATH, SecurityPageName } from '../../common/constants'; @@ -22,7 +22,7 @@ const casesLinks = getCasesDeepLinks({ capabilities: [`${CASES_FEATURE_ID}.${READ_CASES_CAPABILITY}`], }, [SecurityPageName.caseConfigure]: { - capabilities: [`${CASES_FEATURE_ID}.${UPDATE_CASES_CAPABILITY}`], + capabilities: [`${CASES_FEATURE_ID}.${CASES_SETTINGS_CAPABILITY}`], sideNavDisabled: true, }, [SecurityPageName.caseCreate]: { diff --git a/x-pack/plugins/security_solution/public/cases/pages/index.tsx b/x-pack/plugins/security_solution/public/cases/pages/index.tsx index dd639862e2812..ab5b170e423c0 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/index.tsx @@ -21,7 +21,7 @@ import { TimelineId } from '../../../common/types/timeline'; import { getRuleDetailsUrl, useFormatUrl } from '../../common/components/link_to'; -import { useGetUserCasesPermissions, useKibana, useNavigation } from '../../common/lib/kibana'; +import { useKibana, useNavigation } from '../../common/lib/kibana'; import { APP_ID, CASES_PATH, @@ -56,7 +56,7 @@ const TimelineDetailsPanel = () => { const CaseContainerComponent: React.FC = () => { const { cases } = useKibana().services; const { getAppUrl, navigateTo } = useNavigation(); - const userCasesPermissions = useGetUserCasesPermissions(); + const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); const dispatch = useDispatch(); const { formatUrl: detectionsFormatUrl, search: detectionsUrlSearch } = useFormatUrl( SecurityPageName.rules diff --git a/x-pack/plugins/security_solution/public/cases_test_utils.ts b/x-pack/plugins/security_solution/public/cases_test_utils.ts index d177934cb02ee..dc70dcab33eaa 100644 --- a/x-pack/plugins/security_solution/public/cases_test_utils.ts +++ b/x-pack/plugins/security_solution/public/cases_test_utils.ts @@ -5,34 +5,39 @@ * 2.0. */ -export const noCasesCapabilities = () => ({ +import type { CasesPermissions, CasesCapabilities } from '@kbn/cases-plugin/common'; + +export const noCasesCapabilities = (): CasesCapabilities => ({ create_cases: false, read_cases: false, update_cases: false, delete_cases: false, push_cases: false, - cases_connector: false, + cases_connectors: false, + cases_settings: false, }); -export const readCasesCapabilities = () => ({ +export const readCasesCapabilities = (): CasesCapabilities => ({ create_cases: false, read_cases: true, update_cases: false, delete_cases: false, push_cases: false, - cases_connector: true, + cases_connectors: true, + cases_settings: false, }); -export const allCasesCapabilities = () => ({ +export const allCasesCapabilities = (): CasesCapabilities => ({ create_cases: true, read_cases: true, update_cases: true, delete_cases: true, push_cases: true, - cases_connector: true, + cases_connectors: true, + cases_settings: true, }); -export const noCasesPermissions = () => ({ +export const noCasesPermissions = (): CasesPermissions => ({ all: false, create: false, read: false, @@ -40,9 +45,10 @@ export const noCasesPermissions = () => ({ delete: false, push: false, connectors: false, + settings: false, }); -export const readCasesPermissions = () => ({ +export const readCasesPermissions = (): CasesPermissions => ({ all: false, create: false, read: true, @@ -50,9 +56,10 @@ export const readCasesPermissions = () => ({ delete: false, push: false, connectors: true, + settings: false, }); -export const writeCasesPermissions = () => ({ +export const writeCasesPermissions = (): CasesPermissions => ({ all: false, create: true, read: false, @@ -60,9 +67,10 @@ export const writeCasesPermissions = () => ({ delete: true, push: true, connectors: true, + settings: true, }); -export const allCasesPermissions = () => ({ +export const allCasesPermissions = (): CasesPermissions => ({ all: true, create: true, read: true, @@ -70,4 +78,5 @@ export const allCasesPermissions = () => ({ delete: true, push: true, connectors: true, + settings: true, }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx index edc72e92ff153..2718c3adf2012 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx @@ -26,7 +26,7 @@ import { mockAlertDetailsData } from './__mocks__'; import type { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; import { TimelineTabs } from '../../../../common/types/timeline'; import { useInvestigationTimeEnrichment } from '../../containers/cti/event_enrichment'; -import { useGetUserCasesPermissions, useKibana } from '../../lib/kibana'; +import { useKibana } from '../../lib/kibana'; import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; @@ -44,14 +44,8 @@ jest.mock('../../../timelines/components/timeline/body/renderers', () => { }); jest.mock('../../lib/kibana'); -const originalKibanaLib = jest.requireActual('../../lib/kibana'); const useKibanaMock = useKibana as jest.Mocked; -// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object -// The returned permissions object will indicate that the user does not have permissions by default -const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock; -mockUseGetUserCasesPermissions.mockImplementation(originalKibanaLib.useGetUserCasesPermissions); - jest.mock('../../containers/cti/event_enrichment'); jest.mock('../../../detection_engine/rule_management/logic/use_rule_with_fallback', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.test.tsx index 2ef1277884c10..b9b132763d6f7 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.test.tsx @@ -12,7 +12,6 @@ import { TestProviders } from '../../../mock'; import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline'; import { useKibana as mockUseKibana } from '../../../lib/kibana/__mocks__'; -import { useGetUserCasesPermissions } from '../../../lib/kibana'; import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features'; import { licenseService } from '../../../hooks/use_license'; import { noCasesPermissions, readCasesPermissions } from '../../../../cases_test_utils'; @@ -20,12 +19,13 @@ import { Insights } from './insights'; import * as i18n from './translations'; const mockedUseKibana = mockUseKibana(); +const mockCanUseCases = jest.fn(); + jest.mock('../../../lib/kibana', () => { const original = jest.requireActual('../../../lib/kibana'); return { ...original, - useGetUserCasesPermissions: jest.fn(), useToasts: jest.fn().mockReturnValue({ addWarning: jest.fn() }), useKibana: () => ({ ...mockedUseKibana, @@ -35,12 +35,12 @@ jest.mock('../../../lib/kibana', () => { api: { getRelatedCases: jest.fn(), }, + helpers: { canUseCases: mockCanUseCases }, }, }, }), }; }); -const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock; jest.mock('../../../hooks/use_license', () => { const licenseServiceInstance = { @@ -94,7 +94,7 @@ const data: TimelineEventsDetailsItem[] = [ describe('Insights', () => { beforeEach(() => { - mockUseGetUserCasesPermissions.mockReturnValue(noCasesPermissions()); + mockCanUseCases.mockReturnValue(noCasesPermissions()); }); it('does not render when there is no content to show', () => { @@ -116,7 +116,7 @@ describe('Insights', () => { // It will show for all users that are able to read case data. // Enabling that permission, will show the case insight module which // is necessary to pass this test. - mockUseGetUserCasesPermissions.mockReturnValue(readCasesPermissions()); + mockCanUseCases.mockReturnValue(readCasesPermissions()); render( diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.tsx index e4e4f317467e2..60c89438aa12d 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.tsx @@ -11,12 +11,12 @@ import { euiStyled } from '@kbn/kibana-react-plugin/common'; import { ALERT_SUPPRESSION_DOCS_COUNT } from '@kbn/rule-data-utils'; import { find } from 'lodash/fp'; +import { APP_ID } from '../../../../../common'; import * as i18n from './translations'; import type { BrowserFields } from '../../../containers/source'; import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline'; import { hasData } from './helpers'; -import { useGetUserCasesPermissions } from '../../../lib/kibana'; import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features'; import { useLicense } from '../../../hooks/use_license'; import { RelatedAlertsByProcessAncestry } from './related_alerts_by_process_ancestry'; @@ -24,6 +24,7 @@ import { RelatedCases } from './related_cases'; import { RelatedAlertsBySourceEvent } from './related_alerts_by_source_event'; import { RelatedAlertsBySession } from './related_alerts_by_session'; import { RelatedAlertsUpsell } from './related_alerts_upsell'; +import { useKibana } from '../../../lib/kibana'; const StyledInsightItem = euiStyled(EuiFlexItem)` border: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; @@ -45,6 +46,7 @@ interface Props { */ export const Insights = React.memo( ({ browserFields, eventId, data, isReadOnly, scopeId }) => { + const { cases } = useKibana().services; const isRelatedAlertsByProcessAncestryEnabled = useIsExperimentalFeatureEnabled( 'insightsRelatedAlertsByProcessAncestry' ); @@ -83,7 +85,7 @@ export const Insights = React.memo( ); const hasAlertSuppressionField = hasData(alertSuppressionField); - const userCasesPermissions = useGetUserCasesPermissions(); + const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); const hasCasesReadPermissions = userCasesPermissions.read; // Make sure that the alert has at least one of the associated fields diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_cases.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_cases.test.tsx index 8e6bc304e1a38..52a6d5eb1eb42 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_cases.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_cases.test.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { TestProviders } from '../../../mock'; import { useKibana as mockUseKibana } from '../../../lib/kibana/__mocks__'; -import { useGetUserCasesPermissions } from '../../../lib/kibana'; import { RelatedCases } from './related_cases'; import { noCasesPermissions, readCasesPermissions } from '../../../../cases_test_utils'; import { CASES_LOADING, CASES_COUNT } from './translations'; @@ -19,13 +18,14 @@ import { AlertsCasesTourSteps } from '../../guided_onboarding_tour/tour_config'; const mockedUseKibana = mockUseKibana(); const mockGetRelatedCases = jest.fn(); +const mockCanUseCases = jest.fn(); + jest.mock('../../guided_onboarding_tour'); jest.mock('../../../lib/kibana', () => { const original = jest.requireActual('../../../lib/kibana'); return { ...original, - useGetUserCasesPermissions: jest.fn(), useToasts: jest.fn().mockReturnValue({ addWarning: jest.fn() }), useKibana: () => ({ ...mockedUseKibana, @@ -35,6 +35,7 @@ jest.mock('../../../lib/kibana', () => { api: { getRelatedCases: mockGetRelatedCases, }, + helpers: { canUseCases: mockCanUseCases }, }, }, }), @@ -47,7 +48,7 @@ window.HTMLElement.prototype.scrollIntoView = scrollToMock; describe('Related Cases', () => { beforeEach(() => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(readCasesPermissions()); + mockCanUseCases.mockReturnValue(readCasesPermissions()); (useTourContext as jest.Mock).mockReturnValue({ activeStep: AlertsCasesTourSteps.viewCase, incrementStep: () => null, @@ -58,7 +59,7 @@ describe('Related Cases', () => { }); describe('When user does not have cases read permissions', () => { beforeEach(() => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(noCasesPermissions()); + mockCanUseCases.mockReturnValue(noCasesPermissions()); }); test('should not show related cases when user does not have permissions', async () => { await act(async () => { diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx index cef3117cf28c5..903f1f7c548c9 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx @@ -22,7 +22,6 @@ import { useTimelineEvents } from './use_timelines_events'; import { getDefaultControlColumn } from '../../../timelines/components/timeline/body/control_columns'; import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; import type { UseFieldBrowserOptionsProps } from '../../../timelines/components/fields_browser'; -import { useGetUserCasesPermissions } from '../../lib/kibana'; import { TableId } from '@kbn/securitysolution-data-table'; import { mount } from 'enzyme'; @@ -38,13 +37,6 @@ jest.mock('react-redux', () => { }; }); -const originalKibanaLib = jest.requireActual('../../lib/kibana'); - -// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object -// The returned permissions object will indicate that the user does not have permissions by default -const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock; -mockUseGetUserCasesPermissions.mockImplementation(originalKibanaLib.useGetUserCasesPermissions); - jest.mock('./use_timelines_events'); jest.mock('../../utils/normalize_time_range'); diff --git a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.test.tsx index 7765c4dc0f79d..afe1d0f208765 100644 --- a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.test.tsx @@ -11,21 +11,12 @@ import { TestProviders } from '../../mock'; import { TEST_ID, SessionsView, defaultSessionsFilter } from '.'; import type { EntityType } from '@kbn/timelines-plugin/common'; import type { SessionsComponentsProps } from './types'; -import { useGetUserCasesPermissions } from '../../lib/kibana'; import { TableId } from '@kbn/securitysolution-data-table'; import { licenseService } from '../../hooks/use_license'; import { mount } from 'enzyme'; import type { EventsViewerProps } from '../events_viewer'; jest.mock('../../lib/kibana'); - -const originalKibanaLib = jest.requireActual('../../lib/kibana'); - -// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object -// The returned permissions object will indicate that the user does not have permissions by default -const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock; -mockUseGetUserCasesPermissions.mockImplementation(originalKibanaLib.useGetUserCasesPermissions); - jest.mock('../../utils/normalize_time_range'); const startDate = '2022-03-22T22:10:56.794Z'; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/actions.test.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/actions.test.tsx index aa168343cdb90..924b1158593a7 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/actions.test.tsx @@ -101,6 +101,7 @@ describe('VisualizationActions', () => { .fn() .mockReturnValue({ open: mockGetCreateCaseFlyoutOpen }), }, + helpers: { canUseCases: jest.fn().mockReturnValue(allCasesPermissions()) }, }, application: { capabilities: { [CASES_FEATURE_ID]: allCasesCapabilities() }, diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.test.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.test.tsx index 6118bc4441420..cc03f80daf95b 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.test.tsx @@ -8,7 +8,6 @@ import { renderHook } from '@testing-library/react-hooks'; import { useKibana as mockUseKibana } from '../../lib/kibana/__mocks__'; import { kpiHostMetricLensAttributes } from './lens_attributes/hosts/kpi_host_metric'; import { useAddToExistingCase } from './use_add_to_existing_case'; -import { useGetUserCasesPermissions } from '../../lib/kibana'; import { allCasesPermissions, readCasesPermissions, @@ -18,13 +17,13 @@ import { AttachmentType } from '@kbn/cases-plugin/common'; const mockedUseKibana = mockUseKibana(); const mockGetUseCasesAddToExistingCaseModal = jest.fn(); +const mockCanUseCases = jest.fn(); jest.mock('../../lib/kibana', () => { const original = jest.requireActual('../../lib/kibana'); return { ...original, - useGetUserCasesPermissions: jest.fn(), useKibana: () => ({ ...mockedUseKibana, services: { @@ -33,6 +32,7 @@ jest.mock('../../lib/kibana', () => { hooks: { useCasesAddToExistingCaseModal: mockGetUseCasesAddToExistingCaseModal, }, + helpers: { canUseCases: mockCanUseCases }, }, }, }), @@ -47,7 +47,7 @@ describe('useAddToExistingCase', () => { }; beforeEach(() => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(allCasesPermissions()); + mockCanUseCases.mockReturnValue(allCasesPermissions()); }); it('useCasesAddToExistingCaseModal with attachments', () => { @@ -68,7 +68,7 @@ describe('useAddToExistingCase', () => { }); it("disables the button if the user can't create but can read", () => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(readCasesPermissions()); + mockCanUseCases.mockReturnValue(readCasesPermissions()); const { result } = renderHook(() => useAddToExistingCase({ @@ -81,7 +81,7 @@ describe('useAddToExistingCase', () => { }); it("disables the button if the user can't read but can create", () => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(writeCasesPermissions()); + mockCanUseCases.mockReturnValue(writeCasesPermissions()); const { result } = renderHook(() => useAddToExistingCase({ diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.tsx index 9a9c239c65e91..8f28e9534e6a7 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.tsx @@ -8,7 +8,8 @@ import { useCallback, useMemo } from 'react'; import { AttachmentType, LENS_ATTACHMENT_TYPE } from '@kbn/cases-plugin/common'; import type { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public'; -import { useKibana, useGetUserCasesPermissions } from '../../lib/kibana'; +import { APP_ID } from '../../../../common'; +import { useKibana } from '../../lib/kibana'; import { ADD_TO_CASE_SUCCESS } from './translations'; import type { LensAttributes } from './types'; @@ -21,8 +22,8 @@ export const useAddToExistingCase = ({ lensAttributes: LensAttributes | null; timeRange: { from: string; to: string } | null; }) => { - const userCasesPermissions = useGetUserCasesPermissions(); const { cases } = useKibana().services; + const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); const attachments = useMemo(() => { return [ { diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.test.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.test.tsx index 29969d489a038..91347dc9fe073 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.test.tsx @@ -8,7 +8,6 @@ import { renderHook } from '@testing-library/react-hooks'; import { useKibana as mockUseKibana } from '../../lib/kibana/__mocks__'; import { kpiHostMetricLensAttributes } from './lens_attributes/hosts/kpi_host_metric'; import { useAddToNewCase } from './use_add_to_new_case'; -import { useGetUserCasesPermissions } from '../../lib/kibana'; import { allCasesPermissions, readCasesPermissions, @@ -20,13 +19,13 @@ jest.mock('../../lib/kibana/kibana_react'); const mockedUseKibana = mockUseKibana(); const mockGetUseCasesAddToNewCaseFlyout = jest.fn(); +const mockCanUseCases = jest.fn(); jest.mock('../../lib/kibana', () => { const original = jest.requireActual('../../lib/kibana'); return { ...original, - useGetUserCasesPermissions: jest.fn(), useKibana: () => ({ ...mockedUseKibana, services: { @@ -35,6 +34,7 @@ jest.mock('../../lib/kibana', () => { hooks: { useCasesAddToNewCaseFlyout: mockGetUseCasesAddToNewCaseFlyout, }, + helpers: { canUseCases: mockCanUseCases }, }, }, }), @@ -47,7 +47,7 @@ describe('useAddToNewCase', () => { to: '2022-03-07T15:59:59.999Z', }; beforeEach(() => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(allCasesPermissions()); + mockCanUseCases.mockReturnValue(allCasesPermissions()); }); it('useCasesAddToNewCaseFlyout with attachments', () => { @@ -64,7 +64,7 @@ describe('useAddToNewCase', () => { }); it("disables the button if the user can't create but can read", () => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(readCasesPermissions()); + mockCanUseCases.mockReturnValue(readCasesPermissions()); const { result } = renderHook(() => useAddToNewCase({ @@ -76,7 +76,7 @@ describe('useAddToNewCase', () => { }); it("disables the button if the user can't read but can create", () => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(writeCasesPermissions()); + mockCanUseCases.mockReturnValue(writeCasesPermissions()); const { result } = renderHook(() => useAddToNewCase({ diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.tsx index 6a395af34b445..68f730b376dcf 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.tsx @@ -8,7 +8,8 @@ import { useCallback, useMemo } from 'react'; import { AttachmentType, LENS_ATTACHMENT_TYPE } from '@kbn/cases-plugin/common'; import type { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public'; -import { useKibana, useGetUserCasesPermissions } from '../../lib/kibana'; +import { APP_ID } from '../../../../common'; +import { useKibana } from '../../lib/kibana'; import { ADD_TO_CASE_SUCCESS } from './translations'; import type { LensAttributes } from './types'; @@ -20,8 +21,9 @@ export interface UseAddToNewCaseProps { } export const useAddToNewCase = ({ onClick, timeRange, lensAttributes }: UseAddToNewCaseProps) => { - const userCasesPermissions = useGetUserCasesPermissions(); const { cases } = useKibana().services; + const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); + const attachments = useMemo(() => { return [ { diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts index 954f4fd0b74bc..a0f59fb18f3f8 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts @@ -95,7 +95,6 @@ export const useToasts = jest export const useCurrentUser = jest.fn(); export const withKibana = jest.fn(createWithKibanaMock()); export const KibanaContextProvider = jest.fn(createKibanaContextProviderMock()); -export const useGetUserCasesPermissions = jest.fn(); export const useAppUrl = jest.fn().mockReturnValue({ getAppUrl: jest .fn() diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts index c1e48a8a9ba98..714049872ee5d 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts @@ -14,7 +14,6 @@ import { camelCase, isArray, isObject } from 'lodash'; import { set } from '@kbn/safer-lodash-set'; import type { AuthenticatedUser } from '@kbn/security-plugin/common'; import type { Capabilities } from '@kbn/core/public'; -import type { CasesPermissions } from '@kbn/cases-plugin/common'; import { useGetAppUrl, useNavigateTo, @@ -22,11 +21,7 @@ import { type GetAppUrl, type NavigateTo, } from '@kbn/security-solution-navigation'; -import { - CASES_FEATURE_ID, - DEFAULT_DATE_FORMAT, - DEFAULT_DATE_FORMAT_TZ, -} from '../../../../common/constants'; +import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../../common/constants'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import type { StartServices } from '../../../types'; import { useUiSetting, useKibana } from './kibana_react'; @@ -153,44 +148,6 @@ export const useCurrentUser = (): AuthenticatedElasticUser | null => { return user; }; -export const useGetUserCasesPermissions = () => { - const [casesPermissions, setCasesPermissions] = useState({ - all: false, - create: false, - read: false, - update: false, - delete: false, - push: false, - connectors: false, - }); - const uiCapabilities = useKibana().services.application.capabilities; - const casesCapabilities = useKibana().services.cases.helpers.getUICapabilities( - uiCapabilities[CASES_FEATURE_ID] - ); - - useEffect(() => { - setCasesPermissions({ - all: casesCapabilities.all, - create: casesCapabilities.create, - read: casesCapabilities.read, - update: casesCapabilities.update, - delete: casesCapabilities.delete, - push: casesCapabilities.push, - connectors: casesCapabilities.connectors, - }); - }, [ - casesCapabilities.all, - casesCapabilities.create, - casesCapabilities.read, - casesCapabilities.update, - casesCapabilities.delete, - casesCapabilities.push, - casesCapabilities.connectors, - ]); - - return casesPermissions; -}; - export const useAppUrl = useGetAppUrl; export { useNavigateTo, useNavigation }; export type { GetAppUrl, NavigateTo }; diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts index 3d6d1a7034d28..ffbd26b97028a 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts @@ -117,7 +117,7 @@ export const createStartServicesMock = ( const discover = discoverPluginMock.createStartContract(); const cases = mockCasesContract(); const dataViewServiceMock = dataViewPluginMocks.createStartContract(); - cases.helpers.getUICapabilities.mockReturnValue(noCasesPermissions()); + cases.helpers.canUseCases.mockReturnValue(noCasesPermissions()); const triggersActionsUi = triggersActionsUiMock.createStart(); const cloudExperiments = cloudExperimentsMock.createStartMock(); const guidedOnboarding = guidedOnboardingMock.createStart(); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx index 2b887808696bd..a8e13bb8c5c27 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx @@ -74,17 +74,22 @@ jest.mock('../../../../common/lib/kibana', () => { application: { capabilities: { siem: { crud_alerts: true, read_alerts: true } }, }, - cases: mockCasesContract(), + cases: { + ...mockCasesContract(), + helpers: { + canUseCases: jest.fn().mockReturnValue({ + all: true, + create: true, + read: true, + update: true, + delete: true, + push: true, + }), + getRuleIdFromEvent: jest.fn(), + }, + }, }, }), - useGetUserCasesPermissions: jest.fn().mockReturnValue({ - all: true, - create: true, - read: true, - update: true, - delete: true, - push: true, - }), }; }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.test.tsx index de33379f48aba..cbe56a62c4574 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.test.tsx @@ -12,7 +12,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { useAddToCaseActions } from './use_add_to_case_actions'; import { TestProviders } from '../../../../common/mock'; -import { useGetUserCasesPermissions, useKibana } from '../../../../common/lib/kibana'; +import { useKibana } from '../../../../common/lib/kibana'; import { useTourContext } from '../../../../common/components/guided_onboarding_tour'; import { AlertsCasesTourSteps, @@ -20,6 +20,7 @@ import { } from '../../../../common/components/guided_onboarding_tour/tour_config'; import { CasesTourSteps } from '../../../../common/components/guided_onboarding_tour/cases_tour_steps'; import type { AlertTableContextMenuItem } from '../types'; +import { allCasesPermissions } from '../../../../cases_test_utils'; jest.mock('../../../../common/components/guided_onboarding_tour'); jest.mock('../../../../common/lib/kibana'); @@ -76,15 +77,6 @@ describe('useAddToCaseActions', () => { isTourShown: () => false, }); - (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ - all: true, - create: true, - read: true, - update: true, - delete: true, - push: true, - }); - useKibanaMock.mockReturnValue({ services: { cases: { @@ -94,6 +86,7 @@ describe('useAddToCaseActions', () => { }, helpers: { getRuleIdFromEvent: () => null, + canUseCases: jest.fn().mockReturnValue(allCasesPermissions()), }, }, }, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx index 821a638e893c2..de3c8782722fa 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx @@ -9,6 +9,7 @@ import React, { useCallback, useMemo } from 'react'; import { AttachmentType } from '@kbn/cases-plugin/common'; import type { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; +import { APP_ID } from '../../../../../common'; import { CasesTourSteps } from '../../../../common/components/guided_onboarding_tour/cases_tour_steps'; import { AlertsCasesTourSteps, @@ -16,7 +17,7 @@ import { SecurityStepId, } from '../../../../common/components/guided_onboarding_tour/tour_config'; import { useTourContext } from '../../../../common/components/guided_onboarding_tour'; -import { useGetUserCasesPermissions, useKibana } from '../../../../common/lib/kibana'; +import { useKibana } from '../../../../common/lib/kibana'; import type { TimelineNonEcsData } from '../../../../../common/search_strategy'; import { ADD_TO_EXISTING_CASE, ADD_TO_NEW_CASE } from '../translations'; import type { AlertTableContextMenuItem } from '../types'; @@ -43,7 +44,7 @@ export const useAddToCaseActions = ({ refetch, }: UseAddToCaseActions) => { const { cases: casesUi } = useKibana().services; - const userCasesPermissions = useGetUserCasesPermissions(); + const userCasesPermissions = casesUi.helpers.canUseCases([APP_ID]); const isAlert = useMemo(() => { return ecsData?.event?.kind?.includes('signal'); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.test.tsx index 3bee45e94712c..9a490bec1ce25 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.test.tsx @@ -23,7 +23,6 @@ import { usePreviewHistogram } from './use_preview_histogram'; import { PreviewHistogram } from './preview_histogram'; import { ALL_VALUES_ZEROS_TITLE } from '../../../../common/components/charts/translation'; -import { useGetUserCasesPermissions } from '../../../../common/lib/kibana'; import { useTimelineEvents } from '../../../../common/components/events_viewer/use_timelines_events'; import { TableId } from '@kbn/securitysolution-data-table'; import { createStore } from '../../../../common/store'; @@ -58,12 +57,7 @@ const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as j const getMockUseIsExperimentalFeatureEnabled = (mockMapping?: Partial) => (flag: keyof typeof allowedExperimentalValues) => mockMapping ? mockMapping?.[flag] : allowedExperimentalValues?.[flag]; -const originalKibanaLib = jest.requireActual('../../../../common/lib/kibana'); -// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object -// The returned permissions object will indicate that the user does not have permissions by default -const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock; -mockUseGetUserCasesPermissions.mockImplementation(originalKibanaLib.useGetUserCasesPermissions); const mockUseFieldBrowserOptions = jest.fn(); jest.mock('../../../../timelines/components/fields_browser', () => ({ useFieldBrowserOptions: (props: UseFieldBrowserOptionsProps) => mockUseFieldBrowserOptions(props), diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx index 356938a6d54e3..cf67bf45fd360 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx @@ -18,7 +18,7 @@ import { TimelineId } from '../../../../common/types/timeline'; import { TestProviders } from '../../../common/mock'; import { mockTimelines } from '../../../common/mock/mock_timelines_plugin'; import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock'; -import { useKibana, useGetUserCasesPermissions, useHttp } from '../../../common/lib/kibana'; +import { useKibana, useHttp } from '../../../common/lib/kibana'; import { mockCasesContract } from '@kbn/cases-plugin/public/mocks'; import { initialUserPrivilegesState as mockInitialUserPrivilegesState } from '../../../common/components/user_privileges/user_privileges_context'; import { useUserPrivileges } from '../../../common/components/user_privileges'; @@ -46,7 +46,6 @@ jest.mock('../user_info', () => ({ })); jest.mock('../../../common/lib/kibana'); -(useGetUserCasesPermissions as jest.Mock).mockReturnValue(allCasesPermissions()); jest.mock('../../containers/detection_engine/alerts/use_alerts_privileges', () => ({ useAlertsPrivileges: jest.fn().mockReturnValue({ hasIndexWrite: true, hasKibanaCRUD: true }), @@ -119,7 +118,13 @@ describe('take action dropdown', () => { services: { ...mockStartServicesMock, timelines: { ...mockTimelines }, - cases: mockCasesContract(), + cases: { + ...mockCasesContract(), + helpers: { + canUseCases: jest.fn().mockReturnValue(allCasesPermissions()), + getRuleIdFromEvent: () => null, + }, + }, osquery: { isOsqueryAvailable: jest.fn().mockReturnValue(true), }, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_cases.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_cases.test.tsx index 00a25ed1885aa..485742aef982e 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_cases.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_cases.test.tsx @@ -7,13 +7,32 @@ import { renderHook } from '@testing-library/react-hooks'; -import { useGetUserCasesPermissions } from '../../../../common/lib/kibana'; +import { useKibana as mockUseKibana } from '../../../../common/lib/kibana/__mocks__'; import { useShowRelatedCases } from './use_show_related_cases'; -jest.mock('../../../../common/lib/kibana'); + +const mockedUseKibana = mockUseKibana(); +const mockCanUseCases = jest.fn(); + +jest.mock('../../../../common/lib/kibana/kibana_react', () => { + const original = jest.requireActual('../../../../common/lib/kibana/kibana_react'); + + return { + ...original, + useKibana: () => ({ + ...mockedUseKibana, + services: { + ...mockedUseKibana.services, + cases: { + helpers: { canUseCases: mockCanUseCases }, + }, + }, + }), + }; +}); describe('useShowRelatedCases', () => { it(`should return false if user doesn't have cases read privilege`, () => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ + mockCanUseCases.mockReturnValue({ all: false, create: false, read: false, @@ -28,7 +47,7 @@ describe('useShowRelatedCases', () => { }); it('should return true if user has cases read privilege', () => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ + mockCanUseCases.mockReturnValue({ all: false, create: false, read: true, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_cases.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_cases.ts index e469cc2ef155c..7bc3429cd0585 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_cases.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_cases.ts @@ -5,12 +5,15 @@ * 2.0. */ -import { useGetUserCasesPermissions } from '../../../../common/lib/kibana'; +import { APP_ID } from '../../../../../common'; +import { useKibana } from '../../../../common/lib/kibana/kibana_react'; /** * Returns true if the user has read privileges for cases, false otherwise */ export const useShowRelatedCases = (): boolean => { - const userCasesPermissions = useGetUserCasesPermissions(); + const { cases } = useKibana().services; + const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); + return userCasesPermissions.read; }; diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx index 5e75ed3643a71..134fac9ac7295 100644 --- a/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx @@ -7,14 +7,14 @@ import React from 'react'; -import { useGetUserCasesPermissions, useKibana } from '../../../common/lib/kibana'; +import { useKibana } from '../../../common/lib/kibana'; import { APP_ID } from '../../../../common/constants'; const MAX_CASES_TO_SHOW = 3; const RecentCasesComponent = () => { const { cases } = useKibana().services; - const userCasesPermissions = useGetUserCasesPermissions(); + const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); return cases.ui.getRecentCases({ permissions: userCasesPermissions, diff --git a/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.test.tsx b/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.test.tsx index 692ffd0b44ec8..8835fc0e48f27 100644 --- a/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.test.tsx @@ -10,7 +10,7 @@ import { mount } from 'enzyme'; import { waitFor } from '@testing-library/react'; import { TestProviders } from '../../../common/mock'; import { Sidebar } from './sidebar'; -import { useGetUserCasesPermissions, useKibana } from '../../../common/lib/kibana'; +import { useKibana } from '../../../common/lib/kibana'; import type { CaseUiClientMock } from '@kbn/cases-plugin/public/mocks'; import { casesPluginMock } from '@kbn/cases-plugin/public/mocks'; import { noCasesPermissions, readCasesPermissions } from '../../../cases_test_utils'; @@ -38,7 +38,7 @@ describe('Sidebar', () => { }); it('does not render the recently created cases section when the user does not have read permissions', async () => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(noCasesPermissions()); + casesMock.helpers.canUseCases.mockReturnValue(noCasesPermissions()); await waitFor(() => mount( @@ -52,7 +52,7 @@ describe('Sidebar', () => { }); it('does render the recently created cases section when the user has read permissions', async () => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(readCasesPermissions()); + casesMock.helpers.canUseCases.mockReturnValue(readCasesPermissions()); await waitFor(() => mount( diff --git a/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.tsx b/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.tsx index 4f87ec1d86605..501e03a65cb02 100644 --- a/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.tsx @@ -8,7 +8,12 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import React, { useMemo } from 'react'; -import { ENABLE_NEWS_FEED_SETTING, NEWS_FEED_URL_SETTING } from '../../../../common/constants'; +import { useKibana } from '../../../common/lib/kibana/kibana_react'; +import { + APP_ID, + ENABLE_NEWS_FEED_SETTING, + NEWS_FEED_URL_SETTING, +} from '../../../../common/constants'; import { Filters as RecentTimelinesFilters } from '../recent_timelines/filters'; import { StatefulRecentTimelines } from '../recent_timelines'; import { StatefulNewsFeed } from '../../../common/components/news_feed'; @@ -17,7 +22,6 @@ import { SidebarHeader } from '../../../common/components/sidebar_header'; import * as i18n from '../../pages/translations'; import { RecentCases } from '../recent_cases'; -import { useGetUserCasesPermissions } from '../../../common/lib/kibana'; const SidebarSpacerComponent = () => ( @@ -30,6 +34,7 @@ export const Sidebar = React.memo<{ recentTimelinesFilterBy: RecentTimelinesFilterMode; setRecentTimelinesFilterBy: (filterBy: RecentTimelinesFilterMode) => void; }>(({ recentTimelinesFilterBy, setRecentTimelinesFilterBy }) => { + const { cases } = useKibana().services; const recentTimelinesFilters = useMemo( () => ( diff --git a/x-pack/plugins/security_solution/public/overview/pages/data_quality.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/data_quality.test.tsx index 5fbe5d80f99ce..3e075d27ac76b 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/data_quality.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/data_quality.test.tsx @@ -30,14 +30,6 @@ jest.mock('../../common/lib/kibana', () => { return { ...original, KibanaServices: mockKibanaServices, - useGetUserCasesPermissions: () => ({ - all: false, - create: false, - read: true, - update: false, - delete: false, - push: false, - }), useKibana: jest.fn(), useUiSetting$: () => ['0,0.[000]'], }; @@ -80,6 +72,16 @@ describe('DataQuality', () => { hooks: { useCasesAddToNewCaseFlyout: jest.fn(), }, + helpers: { + canUseCases: jest.fn().mockReturnValue({ + all: false, + create: false, + read: true, + update: false, + delete: false, + push: false, + }), + }, }, configSettings: { ILMEnabled: true }, }, @@ -307,6 +309,16 @@ describe('DataQuality', () => { hooks: { useCasesAddToNewCaseFlyout: jest.fn(), }, + helpers: { + canUseCases: jest.fn().mockReturnValue({ + all: false, + create: false, + read: true, + update: false, + delete: false, + push: false, + }), + }, }, configSettings: { ILMEnabled: false }, }, diff --git a/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx b/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx index 0ef421977a402..0c318b38a4660 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx @@ -38,15 +38,9 @@ import { HeaderPage } from '../../common/components/header_page'; import { LandingPageComponent } from '../../common/components/landing_page'; import { useLocalStorage } from '../../common/components/local_storage'; import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; -import { DEFAULT_BYTES_FORMAT, DEFAULT_NUMBER_FORMAT } from '../../../common/constants'; +import { APP_ID, DEFAULT_BYTES_FORMAT, DEFAULT_NUMBER_FORMAT } from '../../../common/constants'; import { useSourcererDataView } from '../../common/containers/sourcerer'; -import { - KibanaServices, - useGetUserCasesPermissions, - useKibana, - useToasts, - useUiSetting$, -} from '../../common/lib/kibana'; +import { KibanaServices, useKibana, useToasts, useUiSetting$ } from '../../common/lib/kibana'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { useSignalIndex } from '../../detections/containers/detection_engine/alerts/use_signal_index'; import * as i18n from './translations'; @@ -141,9 +135,7 @@ const DataQualityComponent: React.FC = () => { const httpFetch = KibanaServices.get().http.fetch; const { baseTheme, theme } = useThemes(); const toasts = useToasts(); - const { - services: { telemetry }, - } = useKibana(); + const addSuccessToast = useCallback( (toast: { title: string }) => { toasts.addSuccess(toast); @@ -156,7 +148,7 @@ const DataQualityComponent: React.FC = () => { const [selectedOptions, setSelectedOptions] = useState(defaultOptions); const { indicesExist, loading: isSourcererLoading, selectedPatterns } = useSourcererDataView(); const { signalIndexName, loading: isSignalIndexNameLoading } = useSignalIndex(); - const { configSettings, cases } = useKibana().services; + const { configSettings, cases, telemetry } = useKibana().services; const isILMAvailable = configSettings.ILMEnabled; const [startDate, setStartTime] = useState(); @@ -210,7 +202,7 @@ const DataQualityComponent: React.FC = () => { key: LOCAL_STORAGE_KEY, }); - const userCasesPermissions = useGetUserCasesPermissions(); + const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); const canUserCreateAndReadCases = useCallback( () => userCasesPermissions.create && userCasesPermissions.read, [userCasesPermissions.create, userCasesPermissions.read] diff --git a/x-pack/plugins/security_solution/public/overview/pages/detection_response.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/detection_response.test.tsx index 60ee2d02e6b25..2b5561f58cb5c 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/detection_response.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/detection_response.test.tsx @@ -11,6 +11,7 @@ import { render } from '@testing-library/react'; import { DetectionResponse } from './detection_response'; import { TestProviders } from '../../common/mock'; import { noCasesPermissions, readCasesPermissions } from '../../cases_test_utils'; +import { useKibana as mockUseKibana } from '../../common/lib/kibana/__mocks__'; jest.mock('../components/detection_response/alerts_by_status', () => ({ AlertsByStatus: () =>
, @@ -75,12 +76,24 @@ jest.mock('../../detections/containers/detection_engine/alerts/use_alerts_privil })); const defaultUseCasesPermissionsReturn = readCasesPermissions(); -const mockUseCasesPermissions = jest.fn(() => defaultUseCasesPermissionsReturn); -jest.mock('../../common/lib/kibana/hooks', () => { - const original = jest.requireActual('../../common/lib/kibana/hooks'); + +const mockedUseKibana = mockUseKibana(); +const mockCanUseCases = jest.fn(); + +jest.mock('../../common/lib/kibana', () => { + const original = jest.requireActual('../../common/lib/kibana'); + return { ...original, - useGetUserCasesPermissions: () => mockUseCasesPermissions(), + useKibana: () => ({ + ...mockedUseKibana, + services: { + ...mockedUseKibana.services, + cases: { + helpers: { canUseCases: mockCanUseCases }, + }, + }, + }), }; }); @@ -90,7 +103,7 @@ describe('DetectionResponse', () => { mockUseSourcererDataView.mockReturnValue(defaultUseSourcererReturn); mockUseAlertsPrivileges.mockReturnValue(defaultUseAlertsPrivilegesReturn); mockUseSignalIndex.mockReturnValue(defaultUseSignalIndexReturn); - mockUseCasesPermissions.mockReturnValue(defaultUseCasesPermissionsReturn); + mockCanUseCases.mockReturnValue(defaultUseCasesPermissionsReturn); }); it('should render default page', () => { @@ -197,7 +210,7 @@ describe('DetectionResponse', () => { }); it('should not render cases data sections if the user does not have cases read permission', () => { - mockUseCasesPermissions.mockReturnValue(noCasesPermissions()); + mockCanUseCases.mockReturnValue(noCasesPermissions()); const result = render( @@ -218,7 +231,7 @@ describe('DetectionResponse', () => { }); it('should render page permissions message if the user does not have read permission', () => { - mockUseCasesPermissions.mockReturnValue(noCasesPermissions()); + mockCanUseCases.mockReturnValue(noCasesPermissions()); mockUseAlertsPrivileges.mockReturnValue({ hasKibanaREAD: true, hasIndexRead: false, diff --git a/x-pack/plugins/security_solution/public/overview/pages/detection_response.tsx b/x-pack/plugins/security_solution/public/overview/pages/detection_response.tsx index 8bdc95fc69aab..77bbdbe8816ed 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/detection_response.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/detection_response.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; import type { DocLinks } from '@kbn/doc-links'; +import { APP_ID } from '../../../common'; import { InputsModelId } from '../../common/store/inputs/constants'; import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; import { SocTrends } from '../components/detection_response/soc_trends'; @@ -18,7 +19,6 @@ import { useSourcererDataView } from '../../common/containers/sourcerer'; import { useSignalIndex } from '../../detections/containers/detection_engine/alerts/use_signal_index'; import { useAlertsPrivileges } from '../../detections/containers/detection_engine/alerts/use_alerts_privileges'; import { HeaderPage } from '../../common/components/header_page'; -import { useGetUserCasesPermissions } from '../../common/lib/kibana'; import { LandingPageComponent } from '../../common/components/landing_page'; import { AlertsByStatus } from '../components/detection_response/alerts_by_status'; @@ -31,13 +31,16 @@ import { CasesByStatus } from '../components/detection_response/cases_by_status' import { NoPrivileges } from '../../common/components/no_privileges'; import { FiltersGlobal } from '../../common/components/filters_global'; import { useGlobalFilterQuery } from '../../common/hooks/use_global_filter_query'; +import { useKibana } from '../../common/lib/kibana'; const DetectionResponseComponent = () => { + const { cases } = useKibana().services; const { filterQuery } = useGlobalFilterQuery(); const { indicesExist, loading: isSourcererLoading, sourcererDataView } = useSourcererDataView(); const { signalIndexName } = useSignalIndex(); const { hasKibanaREAD, hasIndexRead } = useAlertsPrivileges(); - const canReadCases = useGetUserCasesPermissions().read; + const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); + const canReadCases = userCasesPermissions.read; const canReadAlerts = hasKibanaREAD && hasIndexRead; const isSocTrendsEnabled = useIsExperimentalFeatureEnabled('socTrendsEnabled'); if (!canReadAlerts && !canReadCases) { diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.test.tsx index 39dc27c540d56..7203e74fe02c1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.test.tsx @@ -7,19 +7,46 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; -import { useKibana, useGetUserCasesPermissions } from '../../../../common/lib/kibana'; import { TestProviders, mockIndexNames, mockIndexPattern } from '../../../../common/mock'; import { useSourcererDataView } from '../../../../common/containers/sourcerer'; import { allCasesPermissions, readCasesPermissions } from '../../../../cases_test_utils'; import { mockBrowserFields } from '../../../../common/containers/source/mock'; import { TimelineActionMenu } from '.'; import { TimelineId, TimelineTabs } from '../../../../../common/types'; +import { useKibana as mockUseKibana } from '../../../../common/lib/kibana/__mocks__'; const mockUseSourcererDataView: jest.Mock = useSourcererDataView as jest.Mock; +const mockedUseKibana = mockUseKibana(); +const mockCanUseCases = jest.fn(); + jest.mock('../../../../common/containers/sourcerer'); -const useKibanaMock = useKibana as jest.Mocked; -jest.mock('../../../../common/lib/kibana'); +jest.mock('../../../../common/lib/kibana/kibana_react', () => { + const original = jest.requireActual('../../../../common/lib/kibana/kibana_react'); + + return { + ...original, + useKibana: () => ({ + ...mockedUseKibana, + services: { + ...mockedUseKibana.services, + cases: { + ...mockedUseKibana.services.cases, + helpers: { canUseCases: mockCanUseCases }, + }, + }, + application: { + capabilities: { + navLinks: {}, + management: {}, + catalogue: {}, + actions: { show: true, crud: true }, + }, + }, + }), + }; +}); + jest.mock('@kbn/i18n-react', () => { const originalModule = jest.requireActual('@kbn/i18n-react'); const FormattedRelative = jest.fn().mockImplementation(() => '20 hours ago'); @@ -41,20 +68,15 @@ describe('Action menu', () => { beforeEach(() => { // Mocking these services is required for the header component to render. mockUseSourcererDataView.mockImplementation(() => sourcererDefaultValue); - useKibanaMock().services.application.capabilities = { - navLinks: {}, - management: {}, - catalogue: {}, - actions: { show: true, crud: true }, - }; }); afterEach(() => { jest.clearAllMocks(); }); + describe('AddToCaseButton', () => { it('renders the button when the user has create and read permissions', () => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(allCasesPermissions()); + mockCanUseCases.mockReturnValue(allCasesPermissions()); render( @@ -70,7 +92,7 @@ describe('Action menu', () => { }); it('does not render the button when the user does not have create permissions', () => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(readCasesPermissions()); + mockCanUseCases.mockReturnValue(readCasesPermissions()); render( diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.tsx index 850078e134129..04f7b2eb0a31e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.tsx @@ -7,7 +7,8 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; -import { useGetUserCasesPermissions } from '../../../../common/lib/kibana'; +import { useKibana } from '../../../../common/lib/kibana/kibana_react'; +import { APP_ID } from '../../../../../common'; import type { TimelineTabs } from '../../../../../common/types'; import { InspectButton } from '../../../../common/components/inspect'; import { InputsModelId } from '../../../../common/store/inputs/constants'; @@ -29,7 +30,9 @@ const TimelineActionMenuComponent = ({ activeTab, isInspectButtonDisabled, }: TimelineActionMenuProps) => { - const userCasesPermissions = useGetUserCasesPermissions(); + const { cases } = useKibana().services; + const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); + return ( { }); jest.mock('../../../../common/lib/kibana'); -const originalKibanaLib = jest.requireActual('../../../../common/lib/kibana'); - -// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object -// The returned permissions object will indicate that the user does not have permissions by default -const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock; -mockUseGetUserCasesPermissions.mockImplementation(originalKibanaLib.useGetUserCasesPermissions); - jest.mock('../../../../common/hooks/use_selector'); const useKibanaMock = useKibana as jest.Mocked; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx index 56b7bafe58ea2..81e520368da6c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx @@ -15,7 +15,7 @@ import { APP_ID, APP_UI_ID } from '../../../../../common/constants'; import { timelineSelectors } from '../../../store/timeline'; import { setInsertTimeline, showTimeline } from '../../../store/timeline/actions'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; -import { useGetUserCasesPermissions, useKibana } from '../../../../common/lib/kibana'; +import { useKibana } from '../../../../common/lib/kibana'; import { TimelineId } from '../../../../../common/types/timeline'; import { TimelineStatus, TimelineType } from '../../../../../common/api/timeline'; import { getCreateCaseUrl, getCaseDetailsUrl } from '../../../../common/components/link_to'; @@ -68,7 +68,7 @@ const AddToCaseButtonComponent: React.FC = ({ timelineId }) => { [dispatch, graphEventId, navigateToApp, savedObjectId, timelineId, timelineTitle] ); - const userCasesPermissions = useGetUserCasesPermissions(); + const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); const handleButtonClick = useCallback(() => { setPopover((currentIsOpen) => !currentIsOpen); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/flyout/footer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/flyout/footer.test.tsx index 91a2904287ffc..5362af38c2413 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/flyout/footer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/flyout/footer.test.tsx @@ -13,11 +13,7 @@ import { TimelineId } from '../../../../../../common/types/timeline'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; import { mockAlertDetailsData } from '../../../../../common/components/event_details/__mocks__'; import type { TimelineEventsDetailsItem } from '../../../../../../common/search_strategy'; -import { - KibanaServices, - useGetUserCasesPermissions, - useKibana, -} from '../../../../../common/lib/kibana'; +import { KibanaServices, useKibana } from '../../../../../common/lib/kibana'; import { coreMock } from '@kbn/core/public/mocks'; import { mockCasesContract } from '@kbn/cases-plugin/public/mocks'; @@ -70,12 +66,6 @@ jest.mock('../../../../../detections/components/user_info', () => ({ })); jest.mock('../../../../../common/lib/kibana'); -const originalKibanaLib = jest.requireActual('../../../../../common/lib/kibana'); - -// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object -// The returned permissions object will indicate that the user does not have permissions by default -const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock; -mockUseGetUserCasesPermissions.mockImplementation(originalKibanaLib.useGetUserCasesPermissions); jest.mock( '../../../../../detections/containers/detection_engine/alerts/use_alerts_privileges', diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.test.tsx index 10a536f69c8d0..508a5caa590f4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.test.tsx @@ -11,11 +11,7 @@ import '../../../../common/mock/match_media'; import { TestProviders } from '../../../../common/mock'; import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; -import { - KibanaServices, - useKibana, - useGetUserCasesPermissions, -} from '../../../../common/lib/kibana'; +import { KibanaServices, useKibana } from '../../../../common/lib/kibana'; import { mockBrowserFields, mockRuntimeMappings } from '../../../../common/containers/source/mock'; import { coreMock } from '@kbn/core/public/mocks'; import { mockCasesContext } from '@kbn/cases-plugin/public/mocks/mock_cases_context'; @@ -156,6 +152,9 @@ describe('event details panel component', () => { ui: { getCasesContext: () => mockCasesContext, }, + cases: { + helpers: { canUseCases: jest.fn().mockReturnValue(allCasesPermissions()) }, + }, }, timelines: { getHoverActions: jest.fn().mockReturnValue({ @@ -168,11 +167,12 @@ describe('event details panel component', () => { }, }, }); - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(allCasesPermissions()); }); + afterEach(() => { jest.clearAllMocks(); }); + test('it renders the take action dropdown in the timeline version', () => { const wrapper = render( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx index ce03812e58fc8..7928a93dc4885 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx @@ -32,7 +32,6 @@ import { defaultRowRenderers } from './body/renderers'; import { useSourcererDataView } from '../../../common/containers/sourcerer'; import { createStore } from '../../../common/store'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; -import { useGetUserCasesPermissions } from '../../../common/lib/kibana'; jest.mock('../../containers', () => ({ useTimelineEvents: jest.fn(), @@ -43,12 +42,6 @@ jest.mock('./tabs_content', () => ({ })); jest.mock('../../../common/lib/kibana'); -const originalKibanaLib = jest.requireActual('../../../common/lib/kibana'); - -// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object -// The returned permissions object will indicate that the user does not have permissions by default -const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock; -mockUseGetUserCasesPermissions.mockImplementation(originalKibanaLib.useGetUserCasesPermissions); jest.mock('../../../common/utils/normalize_time_range'); jest.mock('@kbn/i18n-react', () => { diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index 81cceb6561bd6..04a4177485348 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -22,8 +22,22 @@ export default function ({ getService }: FtrProviderContext) { savedObjectsTagging: ['all', 'read', 'minimal_all', 'minimal_read'], canvas: ['all', 'read', 'minimal_all', 'minimal_read'], maps: ['all', 'read', 'minimal_all', 'minimal_read'], - generalCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'], - observabilityCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'], + generalCases: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'cases_delete', + 'cases_settings', + ], + observabilityCases: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'cases_delete', + 'cases_settings', + ], observabilityAIAssistant: ['all', 'read', 'minimal_all', 'minimal_read'], slo: ['all', 'read', 'minimal_all', 'minimal_read'], fleetv2: ['all', 'read', 'minimal_all', 'minimal_read'], @@ -57,7 +71,14 @@ export default function ({ getService }: FtrProviderContext) { ], uptime: ['all', 'read', 'minimal_all', 'minimal_read', 'elastic_managed_locations_enabled'], securitySolutionAssistant: ['all', 'read', 'minimal_all', 'minimal_read'], - securitySolutionCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'], + securitySolutionCases: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'cases_delete', + 'cases_settings', + ], infrastructure: ['all', 'read', 'minimal_all', 'minimal_read'], logs: ['all', 'read', 'minimal_all', 'minimal_read'], apm: ['all', 'read', 'minimal_all', 'minimal_read'], diff --git a/x-pack/test/api_integration/apis/security/privileges_basic.ts b/x-pack/test/api_integration/apis/security/privileges_basic.ts index 174ac2a3c8f66..2773adfe070e8 100644 --- a/x-pack/test/api_integration/apis/security/privileges_basic.ts +++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts @@ -98,8 +98,22 @@ export default function ({ getService }: FtrProviderContext) { savedObjectsTagging: ['all', 'read', 'minimal_all', 'minimal_read'], canvas: ['all', 'read', 'minimal_all', 'minimal_read'], maps: ['all', 'read', 'minimal_all', 'minimal_read'], - generalCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'], - observabilityCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'], + generalCases: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'cases_delete', + 'cases_settings', + ], + observabilityCases: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'cases_delete', + 'cases_settings', + ], observabilityAIAssistant: ['all', 'read', 'minimal_all', 'minimal_read'], slo: ['all', 'read', 'minimal_all', 'minimal_read'], fleetv2: ['all', 'read', 'minimal_all', 'minimal_read'], @@ -139,7 +153,14 @@ export default function ({ getService }: FtrProviderContext) { 'minimal_read', ], securitySolutionAssistant: ['all', 'read', 'minimal_all', 'minimal_read'], - securitySolutionCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'], + securitySolutionCases: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'cases_delete', + 'cases_settings', + ], infrastructure: ['all', 'read', 'minimal_all', 'minimal_read'], logs: ['all', 'read', 'minimal_all', 'minimal_read'], apm: ['all', 'read', 'minimal_all', 'minimal_read'], diff --git a/x-pack/test/cases_api_integration/common/plugins/security_solution/server/plugin.ts b/x-pack/test/cases_api_integration/common/plugins/security_solution/server/plugin.ts index 9648e89827568..3f46aa016811c 100644 --- a/x-pack/test/cases_api_integration/common/plugins/security_solution/server/plugin.ts +++ b/x-pack/test/cases_api_integration/common/plugins/security_solution/server/plugin.ts @@ -68,13 +68,13 @@ export class FixturePlugin implements Plugin Date: Tue, 28 Nov 2023 10:30:16 +0000 Subject: [PATCH 06/14] Raise detection rule alert telemetry from 1K/1hr to total or timeout. (#170334) ## Summary Currently, telemetry instrumentation for prebuilt rule alerts in the security solution is capped at 1K/1hr. This PR lists the limit with a [PiT](https://www.elastic.co/guide/en/elasticsearch/reference/current/point-in-time-api.html) query, but short circuits with a 15m timeout. ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/lib/telemetry/__mocks__/index.ts | 5 +- .../server/lib/telemetry/receiver.ts | 266 +++++++++++------- .../tasks/prebuilt_rule_alerts.test.ts | 6 +- .../telemetry/tasks/prebuilt_rule_alerts.ts | 100 ++++--- 4 files changed, 224 insertions(+), 153 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts b/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts index 3260a3e188242..4af8d3a9e435d 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts @@ -82,10 +82,12 @@ export const createMockTelemetryReceiver = ( fetchLicenseInfo: jest.fn().mockReturnValue(stubLicenseInfo), copyLicenseFields: jest.fn(), fetchFleetAgents: jest.fn(), + openPointInTime: jest.fn(), + getAlertsIndex: jest.fn().mockReturnValue('alerts-*'), fetchDiagnosticAlerts: jest.fn().mockReturnValue(diagnosticsAlert ?? jest.fn()), fetchEndpointMetrics: jest.fn().mockReturnValue(stubEndpointMetricsResponse), fetchEndpointPolicyResponses: jest.fn(), - fetchPrebuiltRuleAlerts: jest.fn().mockReturnValue(prebuiltRuleAlertsResponse), + fetchPrebuiltRuleAlertsBatch: jest.fn().mockReturnValue(prebuiltRuleAlertsResponse), fetchDetectionRulesPackageVersion: jest.fn(), fetchTrustedApplications: jest.fn(), fetchEndpointList: jest.fn(), @@ -95,7 +97,6 @@ export const createMockTelemetryReceiver = ( buildProcessTree: jest.fn().mockReturnValue(processTreeResponse), fetchTimelineEvents: jest.fn().mockReturnValue(Promise.resolve(stubFetchTimelineEvents())), fetchValueListMetaData: jest.fn(), - getAlertsIndex: jest.fn().mockReturnValue('test-alerts-index'), } as unknown as jest.Mocked; }; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts b/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts index a2360617c48f3..80b90deba6ef5 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts @@ -85,6 +85,14 @@ export interface ITelemetryReceiver { getClusterInfo(): ESClusterInfo | undefined; + fetchClusterInfo(): Promise; + + fetchLicenseInfo(): Promise; + + openPointInTime(indexPattern: string): Promise; + + closePointInTime(pitId: string): Promise; + fetchDetectionRulesPackageVersion(): Promise; fetchFleetAgents(): Promise< @@ -151,9 +159,15 @@ export interface ITelemetryReceiver { per_page: number; }>; - fetchClusterInfo(): Promise; - - fetchLicenseInfo(): Promise; + fetchPrebuiltRuleAlertsBatch( + pitId: string, + searchAfterValue: SortResults | undefined + ): Promise<{ + moreToFetch: boolean; + newPitId: string; + searchAfter: SortResults | undefined; + alerts: TelemetryEvent[]; + }>; copyLicenseFields(lic: ESLicense): { issuer?: string | undefined; @@ -163,8 +177,6 @@ export interface ITelemetryReceiver { type: string; }; - fetchPrebuiltRuleAlerts(): Promise<{ events: TelemetryEvent[]; count: number }>; - fetchTimelineAlerts( index: string, rangeFrom: string, @@ -582,138 +594,188 @@ export class TelemetryReceiver implements ITelemetryReceiver { }; } - /** - * Fetch an overview of detection rule alerts over the last 3 hours. - * Filters out custom rules and endpoint rules. - * @returns total of alerts by rules - */ - public async fetchPrebuiltRuleAlerts() { + public async fetchPrebuiltRuleAlertsBatch( + pitId: string, + searchAfterValue: SortResults | undefined + ) { if (this.esClient === undefined || this.esClient === null) { - throw Error('elasticsearch client is unavailable: cannot retrieve pre-built rule alerts'); + throw Error('es client is unavailable: cannot retrieve pre-built rule alert batches'); } - const query: SearchRequest = { - expand_wildcards: ['open' as const, 'hidden' as const], - index: `${this.alertsIndex}*`, - ignore_unavailable: true, - body: { - size: 1_000, - _source: { - exclude: ['message', 'kibana.alert.rule.note', 'kibana.alert.rule.parameters.note'], - }, - query: { - bool: { - filter: [ - { - bool: { - should: [ - { - bool: { - must_not: { - bool: { - should: [ - { - match_phrase: { - 'kibana.alert.rule.name': 'Malware Prevention Alert', - }, + let newPitId = pitId; + let fetchMore = true; + let searchAfter: SortResults | undefined = searchAfterValue; + const query: ESSearchRequest = { + query: { + bool: { + filter: [ + { + bool: { + should: [ + { + bool: { + must_not: { + bool: { + should: [ + { + match_phrase: { + 'kibana.alert.rule.name': 'Malware Prevention Alert', }, - ], - }, + }, + ], }, }, }, - { - bool: { - must_not: { - bool: { - should: [ - { - match_phrase: { - 'kibana.alert.rule.name': 'Malware Detection Alert', - }, + }, + { + bool: { + must_not: { + bool: { + should: [ + { + match_phrase: { + 'kibana.alert.rule.name': 'Malware Detection Alert', }, - ], - }, + }, + ], }, }, }, - { - bool: { - must_not: { - bool: { - should: [ - { - match_phrase: { - 'kibana.alert.rule.name': 'Ransomware Prevention Alert', - }, + }, + { + bool: { + must_not: { + bool: { + should: [ + { + match_phrase: { + 'kibana.alert.rule.name': 'Ransomware Prevention Alert', }, - ], - }, + }, + ], }, }, }, - { - bool: { - must_not: { - bool: { - should: [ - { - match_phrase: { - 'kibana.alert.rule.name': 'Ransomware Detection Alert', - }, + }, + { + bool: { + must_not: { + bool: { + should: [ + { + match_phrase: { + 'kibana.alert.rule.name': 'Ransomware Detection Alert', }, - ], - }, + }, + ], }, }, }, - ], - }, + }, + ], }, - { - bool: { - should: [ - { - match_phrase: { - 'kibana.alert.rule.parameters.immutable': 'true', - }, + }, + { + bool: { + should: [ + { + match_phrase: { + 'kibana.alert.rule.parameters.immutable': 'true', }, - ], - }, - }, - { - range: { - '@timestamp': { - gte: 'now-1h', - lte: 'now', }, + ], + }, + }, + { + range: { + '@timestamp': { + gte: 'now-1h', + lte: 'now', }, }, - ], - }, - }, - aggs: { - prebuilt_rule_alert_count: { - cardinality: { - field: 'event.id', }, - }, + ], }, }, + track_total_hits: false, + sort: [ + { '@timestamp': { order: 'asc', format: 'strict_date_optional_time_nanos' } }, + { _shard_doc: 'desc' }, + ] as unknown as string[], + pit: { id: pitId }, + search_after: searchAfter, + size: 1_000, }; - const response = await this.esClient.search(query, { meta: true }); - tlog(this.logger, `received prebuilt alerts: (${response.body.hits.hits.length})`); + let response = null; + try { + response = await this.esClient.search(query); + const numOfHits = response?.hits.hits.length; + + if (numOfHits > 0) { + const lastHit = response?.hits.hits[numOfHits - 1]; + searchAfter = lastHit?.sort; + } + + fetchMore = numOfHits > 0 && numOfHits < 1_000; + } catch (e) { + tlog(this.logger, e); + fetchMore = false; + } + + if (response == null) { + return { + moreToFetch: false, + newPitId: pitId, + searchAfter, + alerts: [] as TelemetryEvent[], + }; + } - const telemetryEvents: TelemetryEvent[] = response.body.hits.hits.flatMap((h) => + const alerts: TelemetryEvent[] = response.hits.hits.flatMap((h) => h._source != null ? ([h._source] as TelemetryEvent[]) : [] ); - const aggregations = response.body?.aggregations as unknown as { - prebuilt_rule_alert_count: { value: number }; + if (response?.pit_id != null) { + newPitId = response?.pit_id; + } + + tlog(this.logger, `Prebuilt rule alerts to return: ${alerts.length}`); + + return { + moreToFetch: fetchMore, + newPitId, + searchAfter, + alerts, }; + } + + public async openPointInTime(indexPattern: string) { + if (this.esClient === undefined || this.esClient === null) { + throw Error('es client is unavailable: cannot retrieve pre-built rule alert batches'); + } - return { events: telemetryEvents, count: aggregations?.prebuilt_rule_alert_count.value ?? 0 }; + const keepAlive = '5m'; + const pitId: OpenPointInTimeResponse['id'] = ( + await this.esClient.openPointInTime({ + index: `${indexPattern}*`, + keep_alive: keepAlive, + }) + ).id; + + return pitId; + } + + public async closePointInTime(pitId: string) { + if (this.esClient === undefined || this.esClient === null) { + throw Error('es client is unavailable: cannot retrieve pre-built rule alert batches'); + } + + try { + await this.esClient.closePointInTime({ id: pitId }); + } catch (error) { + tlog(this.logger, `Error trying to close point in time: "${pitId}". Error is: "${error}"`); + } } async fetchTimelineAlerts(index: string, rangeFrom: string, rangeTo: string) { diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.test.ts index de5219a7f1fa2..479ceabd65a3b 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.test.ts @@ -42,11 +42,7 @@ describe('security telemetry - detection rule alerts task test', () => { testTaskExecutionPeriod ); expect(mockTelemetryReceiver.fetchDetectionRulesPackageVersion).toHaveBeenCalled(); - expect(mockTelemetryReceiver.fetchPrebuiltRuleAlerts).toHaveBeenCalled(); - expect(mockTelemetryEventsSender.getTelemetryUsageCluster).toHaveBeenCalled(); - expect(mockTelemetryEventsSender.getTelemetryUsageCluster()?.incrementCounter).toBeCalledTimes( - 1 - ); + expect(mockTelemetryReceiver.fetchPrebuiltRuleAlertsBatch).toHaveBeenCalled(); expect(mockTelemetryEventsSender.sendOnDemand).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts index 0fdc6cf32a69c..0765d1d5bbdae 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts @@ -6,6 +6,7 @@ */ import type { Logger } from '@kbn/core/server'; +import type { SortResults } from '@elastic/elasticsearch/lib/api/types'; import type { ITelemetryEventsSender } from '../sender'; import type { ITelemetryReceiver } from '../receiver'; import type { ESClusterInfo, ESLicense, TelemetryEvent } from '../types'; @@ -15,12 +16,14 @@ import { batchTelemetryRecords, createTaskMetric, processK8sUsernames, tlog } fr import { copyAllowlistedFields, filterList } from '../filterlists'; export function createTelemetryPrebuiltRuleAlertsTaskConfig(maxTelemetryBatch: number) { + const taskVersion = '1.2.0'; + return { type: 'security:telemetry-prebuilt-rule-alerts', title: 'Security Solution - Prebuilt Rule and Elastic ML Alerts Telemetry', interval: '1h', - timeout: '5m', - version: '1.0.0', + timeout: '15m', + version: taskVersion, runTask: async ( taskId: string, logger: Logger, @@ -47,53 +50,62 @@ export function createTelemetryPrebuiltRuleAlertsTaskConfig(maxTelemetryBatch: n : ({} as ESLicense | undefined); const packageInfo = packageVersion.status === 'fulfilled' ? packageVersion.value : undefined; + const index = receiver.getAlertsIndex(); - const { events: telemetryEvents, count: totalPrebuiltAlertCount } = - await receiver.fetchPrebuiltRuleAlerts(); - - sender.getTelemetryUsageCluster()?.incrementCounter({ - counterName: 'telemetry_prebuilt_rule_alerts', - counterType: 'prebuilt_alert_count', - incrementBy: totalPrebuiltAlertCount, - }); - - if (telemetryEvents.length === 0) { - tlog(logger, 'no prebuilt rule alerts retrieved'); - await sender.sendOnDemand(TASK_METRICS_CHANNEL, [ - createTaskMetric(taskName, true, startTime), - ]); + if (index === undefined) { + tlog(logger, `alerts index is not ready yet, skipping telemetry task`); return 0; } - const processedAlerts = telemetryEvents.map( - (event: TelemetryEvent): TelemetryEvent => - copyAllowlistedFields(filterList.prebuiltRulesAlerts, event) - ); - - const sanitizedAlerts = processedAlerts.map( - (event: TelemetryEvent): TelemetryEvent => - processK8sUsernames(clusterInfo?.cluster_uuid, event) - ); - - const enrichedAlerts = sanitizedAlerts.map( - (event: TelemetryEvent): TelemetryEvent => ({ - ...event, - licence_id: licenseInfo?.uid, - cluster_uuid: clusterInfo?.cluster_uuid, - cluster_name: clusterInfo?.cluster_name, - package_version: packageInfo?.version, - }) - ); - - tlog(logger, `sending ${enrichedAlerts.length} elastic prebuilt alerts`); - const batches = batchTelemetryRecords(enrichedAlerts, maxTelemetryBatch); - for (const batch of batches) { - await sender.sendOnDemand(TELEMETRY_CHANNEL_DETECTION_ALERTS, batch); + let fetchMore = true; + let searchAfterValue: SortResults | undefined; + let pitId = await receiver.openPointInTime(index); + + while (fetchMore) { + const { moreToFetch, newPitId, searchAfter, alerts } = + await receiver.fetchPrebuiltRuleAlertsBatch(pitId, searchAfterValue); + + if (alerts.length === 0) { + return 0; + } + + fetchMore = moreToFetch; + searchAfterValue = searchAfter; + pitId = newPitId; + + const processedAlerts = alerts.map( + (event: TelemetryEvent): TelemetryEvent => + copyAllowlistedFields(filterList.prebuiltRulesAlerts, event) + ); + + const sanitizedAlerts = processedAlerts.map( + (event: TelemetryEvent): TelemetryEvent => + processK8sUsernames(clusterInfo?.cluster_uuid, event) + ); + + const enrichedAlerts = sanitizedAlerts.map( + (event: TelemetryEvent): TelemetryEvent => ({ + ...event, + licence_id: licenseInfo?.uid, + cluster_uuid: clusterInfo?.cluster_uuid, + cluster_name: clusterInfo?.cluster_name, + package_version: packageInfo?.version, + task_version: taskVersion, + }) + ); + + tlog(logger, `sending ${enrichedAlerts.length} elastic prebuilt alerts`); + const batches = batchTelemetryRecords(enrichedAlerts, maxTelemetryBatch); + + const promises = batches.map(async (batch) => { + sender.sendOnDemand(TELEMETRY_CHANNEL_DETECTION_ALERTS, batch); + }); + + await Promise.all(promises); } - await sender.sendOnDemand(TASK_METRICS_CHANNEL, [ - createTaskMetric(taskName, true, startTime), - ]); - return enrichedAlerts.length; + + await receiver.closePointInTime(pitId); + return 0; } catch (err) { logger.error('could not complete prebuilt alerts telemetry task'); await sender.sendOnDemand(TASK_METRICS_CHANNEL, [ From 0b0110a2dd8448649ab6b09672bd5f81c4daeba3 Mon Sep 17 00:00:00 2001 From: amyjtechwriter <61687663+amyjtechwriter@users.noreply.github.com> Date: Tue, 28 Nov 2023 11:08:01 +0000 Subject: [PATCH 07/14] [DOCS] Linking reporting and sharing page with configure reporting page (#171977) ## Summary Linking the "[Reporting and Sharing](https://www.elastic.co/guide/en/kibana/current/reporting-settings-kb.html#reporting-advanced-settings)" and the "[Configure reporting](https://www.elastic.co/guide/en/kibana/current/secure-reporting.html)" documentation pages to each other as they contain information that relate to each page. Also contains a small formatting fix to a NOTE on the Configure reporting page. Closes: #169065 --- docs/setup/configuring-reporting.asciidoc | 4 +++- docs/user/reporting/index.asciidoc | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/setup/configuring-reporting.asciidoc b/docs/setup/configuring-reporting.asciidoc index 903f6fdfb5afd..d8ff9e1b202b6 100644 --- a/docs/setup/configuring-reporting.asciidoc +++ b/docs/setup/configuring-reporting.asciidoc @@ -113,7 +113,7 @@ Granting the privilege to generate reports also grants the user the privilege to ==== Grant access with the role API With <> enabled in Reporting, you can also use the {ref}/security-api-put-role.html[role API] to grant access to the {report-features}, using *All* privileges, or sub-feature privileges. -NOTE: this [API request](https://www.elastic.co/guide/en/kibana/current/role-management-api-put.html) needs to be executed against the Kibana API endpoint +NOTE: This link:https://www.elastic.co/guide/en/kibana/current/role-management-api-put.html[API request] needs to be executed against the link:https://www.elastic.co/guide/en/kibana/current/api.html[Kibana API endpoint]. [source, sh] --------------------------------------------------------------- POST :/api/_security/role/custom_reporting_user @@ -229,3 +229,5 @@ For more information, see {ref}/notification-settings.html#ssl-notification-sett . Add one or more users who have access to the {report-features}. + Once you've enabled SSL for {kib}, all requests to the reporting endpoints must include valid credentials. + +For more information on sharing reports, direct links, and more, refer to <>. diff --git a/docs/user/reporting/index.asciidoc b/docs/user/reporting/index.asciidoc index 8d53018dec572..676fb430b9c66 100644 --- a/docs/user/reporting/index.asciidoc +++ b/docs/user/reporting/index.asciidoc @@ -31,6 +31,8 @@ You access the options from the *Share* menu in the toolbar. The sharing options NOTE: For Elastic Cloud deployments, {kib} instances require a minimum of 2GB RAM to generate PDF or PNG reports. To change {kib} sizing, {ess-console}[edit the deployment]. +For more information on how to configure reporting in {kib}, refer to <> + [float] [[manually-generate-reports]] == Create reports From a4aa7117bb04d6ded121237ca7d2cd77c9f93ceb Mon Sep 17 00:00:00 2001 From: Mark Hopkin Date: Tue, 28 Nov 2023 11:29:46 +0000 Subject: [PATCH 08/14] [Entity Analytics] Add risk engine missing privileges callout (#171250) ## Summary _note: this is currently behind the experimental feature flag `riskEnginePrivilegesRouteEnabled`._ Add a callout to the Entity Risk Score Management page if the user doesn't have sufficient privileges. Here is the callout with a user with none of the privileges (missing privileges are dynamically shown) Screenshot 2023-11-21 at 12 52 21 as part of this I have added a route `GET /internal/risk_score/engine/privileges` the response payload looks like this: ``` { "privileges": { "kibana": { "feature_savedObjectsManagement.all": false }, "elasticsearch": { "cluster": { "manage_transform": false, "manage_index_templates": false }, "index": { "risk-score.risk-score-*": { "read": false, "write": false } } } }, "has_all_required": false // does the user have all privileges? } ``` Docs issue for associated documentation changes https://github.com/elastic/security-docs/issues/4307 ### Testing - cypress tests added for the no banner case (user has all privs), and the worst case (user has none of the privs) - API Integration tests added for all of the granular cases - Manual test steps - 1. User has correct privileges - Create a user with all risk engine privileges - navigate to the Entity Risk Score Management page - missing privileges banner should not show - 2. User has missing privileges - Create a user with some or no risk engine privileges - navigate to the Entity Risk Score Management page - banner should show and describe all privileges missing ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-doc-links/src/get_doc_links.ts | 3 + packages/kbn-doc-links/src/types.ts | 3 + .../security_solution/common/constants.ts | 1 + .../common/experimental_features.ts | 12 + .../common/risk_engine/constants.ts | 9 + .../common/test/ess_roles.json | 17 ++ .../security_solution/common/test/index.ts | 1 + .../public/entity_analytics/api/api.ts | 12 + .../api/hooks/use_risk_engine_privileges.ts | 12 + .../risk_engine_privileges_callout/index.tsx | 8 + .../risk_engine_privileges_callout.tsx | 31 +++ .../translations.tsx | 97 ++++++++ .../use_missing_risk_engine_privileges.ts | 81 ++++++ .../components/risk_score_enable_section.tsx | 6 +- .../entity_analytics_management_page.tsx | 6 + .../get_user_risk_engine_privileges.ts | 58 +++++ .../risk_engine/routes/index.ts | 1 + .../routes/risk_engine_privileges_route.ts | 48 ++++ .../risk_engine/schema/risk_score_apis.yml | 33 ++- .../lib/entity_analytics/risk_engine/types.ts | 10 + .../security_solution/server/routes/index.ts | 4 + .../config/ess/config.base.ts | 1 + .../default_license/risk_engine/index.ts | 1 + .../risk_engine/risk_engine_privileges.ts | 234 ++++++++++++++++++ .../entity_analytics/utils/risk_engine.ts | 15 ++ ...tics_management_page_privileges_callout.ts | 57 +++++ .../screens/entity_analytics_management.ts | 5 + 27 files changed, 764 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_engine_privileges.ts create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/components/risk_engine_privileges_callout/index.tsx create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/components/risk_engine_privileges_callout/risk_engine_privileges_callout.tsx create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/components/risk_engine_privileges_callout/translations.tsx create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/components/risk_engine_privileges_callout/use_missing_risk_engine_privileges.ts create mode 100644 x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/get_user_risk_engine_privileges.ts create mode 100644 x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/risk_engine_privileges_route.ts create mode 100644 x-pack/test/security_solution_api_integration/test_suites/entity_analytics/default_license/risk_engine/risk_engine_privileges.ts create mode 100644 x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/entity_analytics_management_page_privileges_callout.ts diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 362f3b391ad01..8234daa4b4454 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -458,6 +458,9 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { privileges: `${SECURITY_SOLUTION_DOCS}endpoint-management-req.html`, manageDetectionRules: `${SECURITY_SOLUTION_DOCS}rules-ui-management.html`, createEsqlRuleType: `${SECURITY_SOLUTION_DOCS}rules-ui-create.html#create-esql-rule`, + entityAnalytics: { + riskScorePrerequisites: `${SECURITY_SOLUTION_DOCS}ers-requirements.html`, + }, }, query: { eql: `${ELASTICSEARCH_DOCS}eql.html`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index 1426cab0b3341..b2298eecd3e17 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -349,6 +349,9 @@ export interface DocLinks { readonly privileges: string; readonly manageDetectionRules: string; readonly createEsqlRuleType: string; + readonly entityAnalytics: { + readonly riskScorePrerequisites: string; + }; }; readonly query: { readonly eql: string; diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 75eda07fa185e..c5870f94bf168 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -259,6 +259,7 @@ export const RISK_ENGINE_STATUS_URL = `${RISK_ENGINE_URL}/status`; export const RISK_ENGINE_INIT_URL = `${RISK_ENGINE_URL}/init`; export const RISK_ENGINE_ENABLE_URL = `${RISK_ENGINE_URL}/enable`; export const RISK_ENGINE_DISABLE_URL = `${RISK_ENGINE_URL}/disable`; +export const RISK_ENGINE_PRIVILEGES_URL = `${RISK_ENGINE_URL}/privileges`; /** * Public Risk Score routes diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 2ada33b7426f1..b6ad057bbeffe 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -114,6 +114,18 @@ export const allowedExperimentalValues = Object.freeze({ * Enables Protection Updates tab in the Endpoint Policy Details page */ protectionUpdatesEnabled: true, + + /** + * Disables the timeline save tour. + * This flag is used to disable the tour in cypress tests. + */ + disableTimelineSaveTour: false, + + /** + * Enables the risk engine privileges route + * and associated callout in the UI + */ + riskEnginePrivilegesRouteEnabled: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/security_solution/common/risk_engine/constants.ts b/x-pack/plugins/security_solution/common/risk_engine/constants.ts index 2d4d208559894..46a5a99a7e21a 100644 --- a/x-pack/plugins/security_solution/common/risk_engine/constants.ts +++ b/x-pack/plugins/security_solution/common/risk_engine/constants.ts @@ -6,3 +6,12 @@ */ export const MAX_SPACES_COUNT = 1; + +export const RISK_ENGINE_REQUIRED_ES_CLUSTER_PRIVILEGES = [ + 'manage_index_templates', + 'manage_transform', +]; + +export const RISK_ENGINE_REQUIRED_ES_INDEX_PRIVILEGES = Object.freeze({ + 'risk-score.risk-score-*': ['read', 'write'], +}); diff --git a/x-pack/plugins/security_solution/common/test/ess_roles.json b/x-pack/plugins/security_solution/common/test/ess_roles.json index d21fe90e2de02..9bf9e1b64aee3 100644 --- a/x-pack/plugins/security_solution/common/test/ess_roles.json +++ b/x-pack/plugins/security_solution/common/test/ess_roles.json @@ -132,5 +132,22 @@ "base": [] } ] + }, + "no_risk_engine_privileges": { + "name": "no_risk_engine_privileges", + "elasticsearch": { + "cluster": [], + "indices": [], + "run_as": [] + }, + "kibana": [ + { + "feature": { + "siem": ["read"] + }, + "spaces": ["*"], + "base": [] + } + ] } } diff --git a/x-pack/plugins/security_solution/common/test/index.ts b/x-pack/plugins/security_solution/common/test/index.ts index ac2fd661320ce..277f54c78e6c5 100644 --- a/x-pack/plugins/security_solution/common/test/index.ts +++ b/x-pack/plugins/security_solution/common/test/index.ts @@ -29,6 +29,7 @@ export enum ROLES { reader = 'reader', hunter = 'hunter', hunter_no_actions = 'hunter_no_actions', + no_risk_engine_privileges = 'no_risk_engine_privileges', } /** diff --git a/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts b/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts index 6b92583b1ddde..a3e324a58dd3b 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts @@ -11,6 +11,7 @@ import { RISK_ENGINE_ENABLE_URL, RISK_ENGINE_DISABLE_URL, RISK_ENGINE_INIT_URL, + RISK_ENGINE_PRIVILEGES_URL, } from '../../../common/constants'; import { KibanaServices } from '../../common/lib/kibana'; @@ -20,6 +21,7 @@ import type { GetRiskEngineStatusResponse, InitRiskEngineResponse, DisableRiskEngineResponse, + RiskEnginePrivilegesResponse, } from '../../../server/lib/entity_analytics/risk_engine/types'; import type { RiskScorePreviewRequestSchema } from '../../../common/risk_engine/risk_score_preview/request_schema'; @@ -85,3 +87,13 @@ export const disableRiskEngine = async (): Promise => method: 'POST', }); }; + +/** + * Get risk engine privileges + */ +export const fetchRiskEnginePrivileges = async (): Promise => { + return KibanaServices.get().http.fetch(RISK_ENGINE_PRIVILEGES_URL, { + version: '1', + method: 'GET', + }); +}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_engine_privileges.ts b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_engine_privileges.ts new file mode 100644 index 0000000000000..2a3ffa40856cb --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_engine_privileges.ts @@ -0,0 +1,12 @@ +/* + * 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 { useQuery } from '@tanstack/react-query'; +import { fetchRiskEnginePrivileges } from '../api'; + +export const useRiskEnginePrivileges = () => { + return useQuery(['GET', 'FETCH_RISK_ENGINE_PRIVILEGES'], fetchRiskEnginePrivileges); +}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_engine_privileges_callout/index.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_engine_privileges_callout/index.tsx new file mode 100644 index 0000000000000..f26814431b364 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_engine_privileges_callout/index.tsx @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { RiskEnginePrivilegesCallOut } from './risk_engine_privileges_callout'; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_engine_privileges_callout/risk_engine_privileges_callout.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_engine_privileges_callout/risk_engine_privileges_callout.tsx new file mode 100644 index 0000000000000..edb6bc11f7217 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_engine_privileges_callout/risk_engine_privileges_callout.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { CallOutMessage } from '../../../common/components/callouts'; +import { CallOutSwitcher } from '../../../common/components/callouts'; +import { MissingPrivilegesCallOutBody, MISSING_PRIVILEGES_CALLOUT_TITLE } from './translations'; +import { useMissingPrivileges } from './use_missing_risk_engine_privileges'; + +export const RiskEnginePrivilegesCallOut = () => { + const privileges = useMissingPrivileges(); + + if (privileges.isLoading || privileges.hasAllRequiredPrivileges) { + return null; + } + + const message: CallOutMessage = { + type: 'primary', + id: `missing-risk-engine-privileges`, + title: MISSING_PRIVILEGES_CALLOUT_TITLE, + description: , + }; + + return ( + message && + ); +}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_engine_privileges_callout/translations.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_engine_privileges_callout/translations.tsx new file mode 100644 index 0000000000000..ed58f50f28279 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_engine_privileges_callout/translations.tsx @@ -0,0 +1,97 @@ +/* + * 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 { EuiCode, EuiLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React from 'react'; +import { useKibana } from '../../../common/lib/kibana'; +import { CommaSeparatedValues } from '../../../detections/components/callouts/missing_privileges_callout/comma_separated_values'; +import type { MissingPrivileges } from './use_missing_risk_engine_privileges'; + +export const MISSING_PRIVILEGES_CALLOUT_TITLE = i18n.translate( + 'xpack.securitySolution.riskEngine.missingPrivilegesCallOut.messageTitle', + { + defaultMessage: 'Insufficient privileges', + } +); + +export const MissingPrivilegesCallOutBody: React.FC = ({ + indexPrivileges, + clusterPrivileges, +}) => { + const { docLinks } = useKibana().services; + + return ( + + + + + ), + }} + /> +

+ ), + indexPrivileges: + indexPrivileges.length > 0 ? ( + <> + +
    + {indexPrivileges.map(([index, missingPrivileges]) => ( +
  • {missingIndexPrivileges(index, missingPrivileges)}
  • + ))} +
+ + ) : null, + clusterPrivileges: + clusterPrivileges.length > 0 ? ( + <> + +
    + {clusterPrivileges.map((privilege) => ( +
  • {privilege}
  • + ))} +
+ + ) : null, + }} + /> + ); +}; + +const missingIndexPrivileges = (index: string, privileges: string[]) => ( + , + index: {index}, + }} + /> +); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_engine_privileges_callout/use_missing_risk_engine_privileges.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_engine_privileges_callout/use_missing_risk_engine_privileges.ts new file mode 100644 index 0000000000000..ec41d7445e578 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_engine_privileges_callout/use_missing_risk_engine_privileges.ts @@ -0,0 +1,81 @@ +/* + * 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 { useMemo } from 'react'; +import type { RiskEnginePrivilegesResponse } from '../../../../server/lib/entity_analytics/risk_engine/types'; +import { useRiskEnginePrivileges } from '../../api/hooks/use_risk_engine_privileges'; +import { + RISK_ENGINE_REQUIRED_ES_CLUSTER_PRIVILEGES, + RISK_ENGINE_REQUIRED_ES_INDEX_PRIVILEGES, +} from '../../../../common/risk_engine'; + +const getMissingIndexPrivileges = ( + privileges: RiskEnginePrivilegesResponse['privileges']['elasticsearch']['index'] +): MissingIndexPrivileges => { + const missingIndexPrivileges: MissingIndexPrivileges = []; + + for (const [indexName, requiredPrivileges] of Object.entries( + RISK_ENGINE_REQUIRED_ES_INDEX_PRIVILEGES + )) { + const missingPrivileges = requiredPrivileges.filter( + (privilege) => !privileges[indexName][privilege] + ); + + if (missingPrivileges.length) { + missingIndexPrivileges.push([indexName, missingPrivileges]); + } + } + + return missingIndexPrivileges; +}; + +export type MissingClusterPrivileges = string[]; +export type MissingIndexPrivileges = Array<[indexName: string, privileges: string[]]>; + +export interface MissingPrivileges { + clusterPrivileges: MissingClusterPrivileges; + indexPrivileges: MissingIndexPrivileges; +} + +export type MissingPrivilegesResponse = + | { isLoading: true } + | { isLoading: false; hasAllRequiredPrivileges: true } + | { isLoading: false; missingPrivileges: MissingPrivileges; hasAllRequiredPrivileges: false }; + +export const useMissingPrivileges = (): MissingPrivilegesResponse => { + const { data: privilegesResponse, isLoading } = useRiskEnginePrivileges(); + + return useMemo(() => { + if (isLoading || !privilegesResponse) { + return { + isLoading: true, + }; + } + + if (privilegesResponse.has_all_required) { + return { + isLoading: false, + hasAllRequiredPrivileges: true, + }; + } + + const { privileges } = privilegesResponse; + const missinIndexPrivileges = getMissingIndexPrivileges(privileges.elasticsearch.index); + const missingClusterPrivileges = RISK_ENGINE_REQUIRED_ES_CLUSTER_PRIVILEGES.filter( + (privilege) => !privileges.elasticsearch.cluster[privilege] + ); + + return { + isLoading: false, + hasAllRequiredPrivileges: false, + missingPrivileges: { + indexPrivileges: missinIndexPrivileges, + clusterPrivileges: missingClusterPrivileges, + }, + }; + }, [isLoading, privilegesResponse]); +}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_enable_section.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_enable_section.tsx index 703ed94a8c617..b5ccc1b8daa63 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_enable_section.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_enable_section.tsx @@ -296,7 +296,11 @@ export const RiskScoreEnableSection = () => { )} {!isUpdateAvailable && ( - {isLoading && } + + {isLoading && ( + + )} + { + const privilegesCalloutEnabled = useIsExperimentalFeatureEnabled( + 'riskEnginePrivilegesRouteEnabled' + ); return ( <> + {privilegesCalloutEnabled && } ( + privileges: Array<{ + privilege: PrivilegeName; + authorized: boolean; + }> +): Record => { + return privileges.reduce>((acc, { privilege, authorized }) => { + acc[privilege] = authorized; + return acc; + }, {}); +}; + +export async function getUserRiskEnginePrivileges( + request: KibanaRequest, + security: SecurityPluginStart +): Promise { + const checkPrivileges = security.authz.checkPrivilegesDynamicallyWithRequest(request); + const { privileges, hasAllRequested } = await checkPrivileges({ + elasticsearch: { + cluster: RISK_ENGINE_REQUIRED_ES_CLUSTER_PRIVILEGES, + index: RISK_ENGINE_REQUIRED_ES_INDEX_PRIVILEGES, + }, + }); + + const clusterPrivilegesByPrivilege = groupPrivilegesByName(privileges.elasticsearch.cluster); + + const indexPrivilegesByIndex = Object.entries(privileges.elasticsearch.index).reduce< + Record> + >((acc, [index, indexPrivileges]) => { + acc[index] = groupPrivilegesByName(indexPrivileges); + return acc; + }, {}); + + return { + privileges: { + elasticsearch: { + cluster: clusterPrivilegesByPrivilege, + index: indexPrivilegesByIndex, + }, + }, + has_all_required: hasAllRequested, + }; +} diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/index.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/index.ts index 1c37efc508f05..fc2c420dcb645 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/index.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/index.ts @@ -10,3 +10,4 @@ export { riskEngineInitRoute } from './risk_engine_init_route'; export { riskEngineEnableRoute } from './risk_engine_enable_route'; export { riskEngineDisableRoute } from './risk_engine_disable_route'; export { riskEngineStatusRoute } from './risk_engine_status_route'; +export { riskEnginePrivilegesRoute } from './risk_engine_privileges_route'; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/risk_engine_privileges_route.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/risk_engine_privileges_route.ts new file mode 100644 index 0000000000000..d035119ec0f1e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/risk_engine_privileges_route.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import type { StartServicesAccessor } from '@kbn/core/server'; +import { RISK_ENGINE_PRIVILEGES_URL, APP_ID } from '../../../../../common/constants'; + +import type { StartPlugins } from '../../../../plugin'; +import type { SecuritySolutionPluginRouter } from '../../../../types'; +import { getUserRiskEnginePrivileges } from '../get_user_risk_engine_privileges'; + +export const riskEnginePrivilegesRoute = ( + router: SecuritySolutionPluginRouter, + getStartServices: StartServicesAccessor +) => { + router.versioned + .get({ + access: 'internal', + path: RISK_ENGINE_PRIVILEGES_URL, + options: { + tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`], + }, + }) + .addVersion({ version: '1', validate: false }, async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + + const [_, { security }] = await getStartServices(); + const body = await getUserRiskEnginePrivileges(request, security); + + try { + return response.ok({ + body, + }); + } catch (e) { + const error = transformError(e); + + return siemResponse.error({ + statusCode: error.statusCode, + body: error.message, + }); + } + }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/schema/risk_score_apis.yml b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/schema/risk_score_apis.yml index c51bf19ebd2b1..d9840840ea2f6 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/schema/risk_score_apis.yml +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/schema/risk_score_apis.yml @@ -93,6 +93,16 @@ paths: application/json: schema: $ref: '#/components/schemas/RiskEngineDisableResponse' + /engine/privileges: + get: + summary: Check if the user has access to the risk engine + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/RiskEnginePrivilegesResponse' components: @@ -436,4 +446,25 @@ components: type: boolean error: type: string - \ No newline at end of file + RiskEnginePrivilegesResponse: + type: object + properties: + privileges: + type: object + properties: + elasticsearch: + type: object + properties: + cluster: + type: object + additionalProperties: + type: boolean + index: + type: object + additionalProperties: + type: object + additionalProperties: + type: boolean + has_all_required: + description: If true then the user has full access to the risk engine + type: boolean diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/types.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/types.ts index f5aeaf4f56428..4315b2638fbd8 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/types.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/types.ts @@ -98,6 +98,16 @@ export interface DisableRiskEngineResponse { success: boolean; } +export interface RiskEnginePrivilegesResponse { + privileges: { + elasticsearch: { + cluster: Record; + index: Record>; + }; + }; + has_all_required: boolean; +} + export interface CalculateRiskScoreAggregations { user?: { after_key: AfterKey; diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index b5b6a5c205e95..d220851c48588 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -80,6 +80,7 @@ import { riskEngineInitRoute, riskEngineEnableRoute, riskEngineStatusRoute, + riskEnginePrivilegesRoute, } from '../lib/entity_analytics/risk_engine/routes'; import { riskScoreCalculationRoute } from '../lib/entity_analytics/risk_engine/routes/risk_score_calculation_route'; @@ -187,5 +188,8 @@ export const initRoutes = ( riskEngineInitRoute(router, getStartServices); riskEngineEnableRoute(router, getStartServices); riskEngineDisableRoute(router, getStartServices); + if (config.experimentalFeatures.riskEnginePrivilegesRouteEnabled) { + riskEnginePrivilegesRoute(router, getStartServices); + } } }; diff --git a/x-pack/test/security_solution_api_integration/config/ess/config.base.ts b/x-pack/test/security_solution_api_integration/config/ess/config.base.ts index b4fbdba6de4c4..185b55e491def 100644 --- a/x-pack/test/security_solution_api_integration/config/ess/config.base.ts +++ b/x-pack/test/security_solution_api_integration/config/ess/config.base.ts @@ -81,6 +81,7 @@ export function createTestConfig(options: CreateTestConfigOptions, testFiles?: s 'previewTelemetryUrlEnabled', 'riskScoringPersistence', 'riskScoringRoutesEnabled', + 'riskEnginePrivilegesRouteEnabled', ])}`, '--xpack.task_manager.poll_interval=1000', `--xpack.actions.preconfigured=${JSON.stringify({ diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/default_license/risk_engine/index.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/default_license/risk_engine/index.ts index 878725cd32f9a..5106151cb8c18 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/default_license/risk_engine/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/default_license/risk_engine/index.ts @@ -15,5 +15,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./risk_scoring_task/task_execution')); loadTestFile(require.resolve('./risk_scoring_task/task_execution_nondefault_spaces')); loadTestFile(require.resolve('./telemetry_usage')); + loadTestFile(require.resolve('./risk_engine_privileges')); }); } diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/default_license/risk_engine/risk_engine_privileges.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/default_license/risk_engine/risk_engine_privileges.ts new file mode 100644 index 0000000000000..d0cc241b559f5 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/default_license/risk_engine/risk_engine_privileges.ts @@ -0,0 +1,234 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; +import type { SecurityService } from '../../../../../../../test/common/services/security/security'; +import { riskEngineRouteHelpersFactoryNoAuth } from '../../utils'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +const USER_PASSWORD = 'changeme'; +const ROLES = [ + { + name: 'security_feature_read', + privileges: { + kibana: [ + { + feature: { + siem: ['read'], + }, + spaces: ['default'], + }, + ], + }, + }, + { + name: 'cluster_manage_index_templates', + privileges: { + elasticsearch: { + cluster: ['manage_index_templates'], + }, + }, + }, + { + name: 'cluster_manage_transform', + privileges: { + elasticsearch: { + cluster: ['manage_transform'], + }, + }, + }, + { + name: 'risk_score_index_read', + privileges: { + elasticsearch: { + indices: [ + { + names: ['risk-score.risk-score-*'], + privileges: ['read'], + }, + ], + }, + }, + }, + { + name: 'risk_score_index_write', + privileges: { + elasticsearch: { + indices: [ + { + names: ['risk-score.risk-score-*'], + privileges: ['write'], + }, + ], + }, + }, + }, +]; + +const ALL_ROLE_NAMES = ROLES.map((role) => role.name); + +const allRolesExcept = (role: string) => ALL_ROLE_NAMES.filter((r) => r !== role); + +const USERNAME_TO_ROLES = { + no_cluster_manage_index_templates: allRolesExcept('cluster_manage_index_templates'), + no_cluster_manage_transform: allRolesExcept('cluster_manage_transform'), + no_risk_score_index_read: allRolesExcept('risk_score_index_read'), + no_risk_score_index_write: allRolesExcept('risk_score_index_write'), + all: ALL_ROLE_NAMES, +}; + +export default ({ getService }: FtrProviderContext) => { + describe('@ess privileges_apis', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const riskEngineRoutesNoAuth = riskEngineRouteHelpersFactoryNoAuth(supertestWithoutAuth); + const logger = getService('log'); + let security: SecurityService; + try { + security = getService('security'); + } catch (e) { + // even though this test doesn't have the @serverless tag I cannot get it to stop running + // with serverless config. This is a hack to skip the test if security service is not available + logger.info( + 'Skipping privileges test as security service not available (likely run with serverless config)' + ); + return; + } + + const createRole = async ({ name, privileges }: { name: string; privileges: any }) => { + return await security.role.create(name, privileges); + }; + + const createUser = async ({ + username, + password, + roles, + }: { + username: string; + password: string; + roles: string[]; + }) => { + return await security.user.create(username, { + password, + roles, + full_name: username.replace('_', ' '), + email: `${username}@elastic.co`, + }); + }; + + async function createPrivilegeTestUsers() { + const rolePromises = ROLES.map((role) => createRole(role)); + + await Promise.all(rolePromises); + const userPromises = Object.entries(USERNAME_TO_ROLES).map(([username, roles]) => + createUser({ username, roles, password: USER_PASSWORD }) + ); + + return Promise.all(userPromises); + } + + const getPrivilegesForUsername = async (username: string) => + riskEngineRoutesNoAuth.privilegesForUser({ + username, + password: USER_PASSWORD, + }); + before(async () => { + await createPrivilegeTestUsers(); + }); + + describe('Risk engine privileges API', () => { + it('should return has_all_required true for user with all risk engine privileges', async () => { + const { body } = await getPrivilegesForUsername('all'); + expect(body.has_all_required).to.eql(true); + expect(body.privileges).to.eql({ + elasticsearch: { + cluster: { + manage_index_templates: true, + manage_transform: true, + }, + index: { + 'risk-score.risk-score-*': { + read: true, + write: true, + }, + }, + }, + }); + }); + it('should return has_all_required false for user with no write access to risk indices', async () => { + const { body } = await getPrivilegesForUsername('no_risk_score_index_write'); + expect(body.has_all_required).to.eql(false); + expect(body.privileges).to.eql({ + elasticsearch: { + cluster: { + manage_index_templates: true, + manage_transform: true, + }, + index: { + 'risk-score.risk-score-*': { + read: true, + write: false, + }, + }, + }, + }); + }); + it('should return has_all_required false for user with no read access to risk indices', async () => { + const { body } = await getPrivilegesForUsername('no_risk_score_index_read'); + expect(body.has_all_required).to.eql(false); + expect(body.privileges).to.eql({ + elasticsearch: { + cluster: { + manage_index_templates: true, + manage_transform: true, + }, + index: { + 'risk-score.risk-score-*': { + read: false, + write: true, + }, + }, + }, + }); + }); + it('should return has_all_required false for user with no cluster manage transform privilege', async () => { + const { body } = await getPrivilegesForUsername('no_cluster_manage_transform'); + expect(body.has_all_required).to.eql(false); + expect(body.privileges).to.eql({ + elasticsearch: { + cluster: { + manage_index_templates: true, + manage_transform: false, + }, + index: { + 'risk-score.risk-score-*': { + read: true, + write: true, + }, + }, + }, + }); + }); + it('should return has_all_required false for user with no cluster manage index templates privilege', async () => { + const { body } = await getPrivilegesForUsername('no_cluster_manage_index_templates'); + expect(body.has_all_required).to.eql(false); + expect(body.privileges).to.eql({ + elasticsearch: { + cluster: { + manage_index_templates: false, + manage_transform: true, + }, + index: { + 'risk-score.risk-score-*': { + read: true, + write: true, + }, + }, + }, + }); + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/risk_engine.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/risk_engine.ts index 48c7763d7a9d6..103577482a771 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/risk_engine.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/risk_engine.ts @@ -21,6 +21,7 @@ import { RISK_ENGINE_DISABLE_URL, RISK_ENGINE_ENABLE_URL, RISK_ENGINE_STATUS_URL, + RISK_ENGINE_PRIVILEGES_URL, } from '@kbn/security-solution-plugin/common/constants'; import { createRule, @@ -504,6 +505,20 @@ export const riskEngineRouteHelpersFactory = ( .expect(200), }); +export const riskEngineRouteHelpersFactoryNoAuth = ( + supertestWithoutAuth: SuperTest.SuperTest, + namespace?: string +) => ({ + privilegesForUser: async ({ username, password }: { username: string; password: string }) => + await supertestWithoutAuth + .get(RISK_ENGINE_PRIVILEGES_URL) + .auth(username, password) + .set('elastic-api-version', '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send() + .expect(200), +}); + export const installLegacyRiskScore = async ({ supertest, }: { diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/entity_analytics_management_page_privileges_callout.ts b/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/entity_analytics_management_page_privileges_callout.ts new file mode 100644 index 0000000000000..a2d2b095d1b38 --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/entity_analytics_management_page_privileges_callout.ts @@ -0,0 +1,57 @@ +/* + * 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 { ROLES } from '@kbn/security-solution-plugin/common/test'; +import { RISK_ENGINE_PRIVILEGES_URL } from '@kbn/security-solution-plugin/common/constants'; +import { + RISK_SCORE_PRIVILEGES_CALLOUT, + RISK_SCORE_STATUS_LOADING, +} from '../../screens/entity_analytics_management'; + +import { login } from '../../tasks/login'; +import { visit } from '../../tasks/navigation'; +import { ENTITY_ANALYTICS_MANAGEMENT_URL } from '../../urls/navigation'; + +const loadPageAsUserWithNoPrivileges = () => { + login(ROLES.no_risk_engine_privileges); + visit(ENTITY_ANALYTICS_MANAGEMENT_URL, { role: ROLES.no_risk_engine_privileges }); +}; + +// this test suite doesn't run on serverless because it requires a custom role +describe( + 'Entity analytics management page - Risk Engine Privileges Callout', + { + tags: ['@ess'], + env: { + ftrConfig: { enableExperimental: ['riskEnginePrivilegesRouteEnabled'] }, + }, + }, + () => { + it('should not show the callout for superuser', () => { + cy.intercept(RISK_ENGINE_PRIVILEGES_URL).as('getPrivileges'); + login(); + visit(ENTITY_ANALYTICS_MANAGEMENT_URL); + cy.wait('@getPrivileges', { timeout: 15000 }); + cy.get(RISK_SCORE_STATUS_LOADING).should('not.exist'); + cy.get(RISK_SCORE_PRIVILEGES_CALLOUT).should('not.exist'); + }); + + it('should show the callout for user without risk engine privileges', () => { + cy.intercept(RISK_ENGINE_PRIVILEGES_URL).as('getPrivileges'); + loadPageAsUserWithNoPrivileges(); + cy.get(RISK_SCORE_STATUS_LOADING).should('not.exist'); + cy.wait('@getPrivileges', { timeout: 15000 }); + cy.get(RISK_SCORE_PRIVILEGES_CALLOUT); + cy.get(RISK_SCORE_PRIVILEGES_CALLOUT).should( + 'contain', + 'Missing read, write privileges for the risk-score.risk-score-* index.' + ); + cy.get(RISK_SCORE_PRIVILEGES_CALLOUT).should('contain', 'manage_index_templates'); + cy.get(RISK_SCORE_PRIVILEGES_CALLOUT).should('contain', 'manage_transform'); + }); + } +); diff --git a/x-pack/test/security_solution_cypress/cypress/screens/entity_analytics_management.ts b/x-pack/test/security_solution_cypress/cypress/screens/entity_analytics_management.ts index ebdabcb67bb1e..2c025efcbe79a 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/entity_analytics_management.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/entity_analytics_management.ts @@ -33,4 +33,9 @@ export const RISK_SCORE_UPDATE_BUTTON = '[data-test-subj="risk-score-update-butt export const RISK_SCORE_STATUS = '[data-test-subj="risk-score-status"]'; +export const RISK_SCORE_STATUS_LOADING = '[data-test-subj="risk-score-status-loading"]'; + +export const RISK_SCORE_PRIVILEGES_CALLOUT = + '[data-test-subj="callout-missing-risk-engine-privileges"]'; + export const RISK_SCORE_SWITCH = '[data-test-subj="risk-score-switch"]'; From 085878c289e169f96143c9b992f2a44b4f6ed906 Mon Sep 17 00:00:00 2001 From: Marta Bondyra <4283304+mbondyra@users.noreply.github.com> Date: Tue, 28 Nov 2023 13:13:43 +0100 Subject: [PATCH 09/14] [Lens][Unified search] Auto expand comboboxes and popovers based on the content (#171573) ## Summary Fixes partially two remaining tasks from https://github.com/elastic/kibana/issues/168753 Fixes partially dataview issue from https://github.com/elastic/kibana/issues/170398 It stretches to maximum approximate 60 characters if any of the labels in the list is of this length. If the content doesn't need the container to stretch, it doesn't do it.
Field picker in Lens minimum width: Screenshot 2023-11-21 at 15 56 03 auto-expanded width: Screenshot 2023-11-21 at 15 58 22
Layer data view picker in Lens Screenshot 2023-11-21 at 16 01 17 Screenshot 2023-11-21 at 15 58 09 Screenshot 2023-11-21 at 15 56 27
Data view picker in Unified Search Screenshot 2023-11-21 at 16 00 29 Screenshot 2023-11-21 at 15 58 04 Screenshot 2023-11-21 at 15 56 20
Data view picker in dashboard Create control flyout Screenshot 2023-11-21 at 16 14 00 Screenshot 2023-11-21 at 15 54 56
Unified search data view select component (tested in maps) Screenshot 2023-11-22 at 14 38 25
Unified search field and value picker Adds `panelMinWidth`, removes the custom flex width change behavior Screenshot 2023-11-22 at 14 40 26 https://github.com/elastic/kibana/assets/4283304/f4f33624-9287-403e-8472-81f705440f97
Discover breakdown field Removes the focus stretching and instead uses the panelMinWidth prop Screenshot 2023-11-21 at 16 46 50 Screenshot 2023-11-21 at 16 48 20
--------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 1 + package.json | 1 + .../.storybook/main.js | 9 ++ .../README.md | 3 + .../index.ts | 9 ++ .../jest.config.js | 13 +++ .../kibana.jsonc | 5 ++ .../package.json | 7 ++ .../calculate_width_from_char_count.test.ts | 21 +++++ .../src/calculate_width_from_char_count.ts | 41 +++++++++ .../src/calculate_width_from_entries.test.ts | 53 +++++++++++ .../src/calculate_width_from_entries.ts | 39 ++++++++ .../src/index.ts | 11 +++ .../tsconfig.json | 19 ++++ .../field_picker/field_picker.test.tsx | 89 +++++++++++++++++++ .../components/field_picker/field_picker.tsx | 53 +++++------ .../tsconfig.json | 1 + .../data_view_picker/data_view_picker.tsx | 2 + src/plugins/presentation_util/tsconfig.json | 3 +- .../public/chart/breakdown_field_selector.tsx | 7 +- src/plugins/unified_histogram/tsconfig.json | 1 + .../dataview_picker/change_dataview.styles.ts | 17 +++- .../dataview_picker/change_dataview.tsx | 4 +- .../filter_editor/phrase_value_input.tsx | 70 +++++++-------- .../filter_editor/phrases_values_input.tsx | 66 +++++++------- .../filter_item/field_input.tsx | 54 +++++------ .../filter_item/filter_item.styles.ts | 3 - .../index_pattern_select.tsx | 8 +- src/plugins/unified_search/tsconfig.json | 1 + tsconfig.base.json | 2 + .../dataview_picker/dataview_picker.tsx | 78 ++++++++-------- .../xy/xy_config_panel/layer_header.tsx | 7 +- x-pack/plugins/lens/tsconfig.json | 5 +- yarn.lock | 4 + 34 files changed, 519 insertions(+), 188 deletions(-) create mode 100644 packages/kbn-calculate-width-from-char-count/.storybook/main.js create mode 100644 packages/kbn-calculate-width-from-char-count/README.md create mode 100644 packages/kbn-calculate-width-from-char-count/index.ts create mode 100644 packages/kbn-calculate-width-from-char-count/jest.config.js create mode 100644 packages/kbn-calculate-width-from-char-count/kibana.jsonc create mode 100644 packages/kbn-calculate-width-from-char-count/package.json create mode 100644 packages/kbn-calculate-width-from-char-count/src/calculate_width_from_char_count.test.ts create mode 100644 packages/kbn-calculate-width-from-char-count/src/calculate_width_from_char_count.ts create mode 100644 packages/kbn-calculate-width-from-char-count/src/calculate_width_from_entries.test.ts create mode 100644 packages/kbn-calculate-width-from-char-count/src/calculate_width_from_entries.ts create mode 100644 packages/kbn-calculate-width-from-char-count/src/index.ts create mode 100644 packages/kbn-calculate-width-from-char-count/tsconfig.json create mode 100644 packages/kbn-visualization-ui-components/components/field_picker/field_picker.test.tsx diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3b4f77f87ff95..6780cf97c1336 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -55,6 +55,7 @@ packages/kbn-bazel-runner @elastic/kibana-operations examples/bfetch_explorer @elastic/appex-sharedux src/plugins/bfetch @elastic/appex-sharedux packages/kbn-calculate-auto @elastic/obs-ux-management-team +packages/kbn-calculate-width-from-char-count @elastic/kibana-visualizations x-pack/plugins/canvas @elastic/kibana-presentation x-pack/test/cases_api_integration/common/plugins/cases @elastic/response-ops packages/kbn-cases-components @elastic/response-ops diff --git a/package.json b/package.json index da2855826068a..92a0ae4f2be74 100644 --- a/package.json +++ b/package.json @@ -167,6 +167,7 @@ "@kbn/bfetch-explorer-plugin": "link:examples/bfetch_explorer", "@kbn/bfetch-plugin": "link:src/plugins/bfetch", "@kbn/calculate-auto": "link:packages/kbn-calculate-auto", + "@kbn/calculate-width-from-char-count": "link:packages/kbn-calculate-width-from-char-count", "@kbn/canvas-plugin": "link:x-pack/plugins/canvas", "@kbn/cases-api-integration-test-plugin": "link:x-pack/test/cases_api_integration/common/plugins/cases", "@kbn/cases-components": "link:packages/kbn-cases-components", diff --git a/packages/kbn-calculate-width-from-char-count/.storybook/main.js b/packages/kbn-calculate-width-from-char-count/.storybook/main.js new file mode 100644 index 0000000000000..8dc3c5d1518f4 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/.storybook/main.js @@ -0,0 +1,9 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = require('@kbn/storybook').defaultConfig; diff --git a/packages/kbn-calculate-width-from-char-count/README.md b/packages/kbn-calculate-width-from-char-count/README.md new file mode 100644 index 0000000000000..13581e81bd9e6 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/README.md @@ -0,0 +1,3 @@ +# @kbn/calculate-width-from-char-count + +This package contains a function that calculates the approximate width of the component from a text length. diff --git a/packages/kbn-calculate-width-from-char-count/index.ts b/packages/kbn-calculate-width-from-char-count/index.ts new file mode 100644 index 0000000000000..de0577ee3ed83 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/index.ts @@ -0,0 +1,9 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './src'; diff --git a/packages/kbn-calculate-width-from-char-count/jest.config.js b/packages/kbn-calculate-width-from-char-count/jest.config.js new file mode 100644 index 0000000000000..0538847bfc820 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/jest.config.js @@ -0,0 +1,13 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../..', + roots: ['/packages/kbn-calculate-width-from-char-count'], +}; diff --git a/packages/kbn-calculate-width-from-char-count/kibana.jsonc b/packages/kbn-calculate-width-from-char-count/kibana.jsonc new file mode 100644 index 0000000000000..216b12ddeac89 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/calculate-width-from-char-count", + "owner": "@elastic/kibana-visualizations" +} diff --git a/packages/kbn-calculate-width-from-char-count/package.json b/packages/kbn-calculate-width-from-char-count/package.json new file mode 100644 index 0000000000000..dd8182452f0ee --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/package.json @@ -0,0 +1,7 @@ +{ + "name": "@kbn/calculate-width-from-char-count", + "private": true, + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0", + "sideEffects": false +} \ No newline at end of file diff --git a/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_char_count.test.ts b/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_char_count.test.ts new file mode 100644 index 0000000000000..1dbe25306b639 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_char_count.test.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { calculateWidthFromCharCount, MAX_WIDTH } from './calculate_width_from_char_count'; + +describe('calculateWidthFromCharCount', () => { + it('should return minimum width if char count is smaller than minWidth', () => { + expect(calculateWidthFromCharCount(10, { minWidth: 300 })).toBe(300); + }); + it('should return calculated width', () => { + expect(calculateWidthFromCharCount(30)).toBe(30 * 7 + 116); + }); + it('should return maximum width if char count is bigger than maxWidth', () => { + expect(calculateWidthFromCharCount(1000)).toBe(MAX_WIDTH); + }); +}); diff --git a/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_char_count.ts b/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_char_count.ts new file mode 100644 index 0000000000000..c79307473c7e8 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_char_count.ts @@ -0,0 +1,41 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export interface LIMITS { + paddingsWidth: number; + minWidth?: number; + avCharWidth: number; + maxWidth: number; +} + +export const MAX_WIDTH = 550; +const PADDINGS_WIDTH = 116; +const AVERAGE_CHAR_WIDTH = 7; + +const defaultPanelWidths: LIMITS = { + maxWidth: MAX_WIDTH, + avCharWidth: AVERAGE_CHAR_WIDTH, + paddingsWidth: PADDINGS_WIDTH, +}; + +export function calculateWidthFromCharCount( + labelLength: number, + overridesPanelWidths?: Partial +) { + const { maxWidth, avCharWidth, paddingsWidth, minWidth } = { + ...defaultPanelWidths, + ...overridesPanelWidths, + }; + const widthForCharCount = paddingsWidth + labelLength * avCharWidth; + + if (minWidth && widthForCharCount < minWidth) { + return minWidth; + } + + return Math.min(widthForCharCount, maxWidth); +} diff --git a/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_entries.test.ts b/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_entries.test.ts new file mode 100644 index 0000000000000..6e740defdce92 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_entries.test.ts @@ -0,0 +1,53 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { calculateWidthFromEntries } from './calculate_width_from_entries'; +import { MAX_WIDTH } from './calculate_width_from_char_count'; +import faker from 'faker'; + +const generateLabel = (length: number) => faker.random.alpha({ count: length }); + +const generateObjectWithLabelOfLength = (length: number, propOverrides?: Record) => ({ + label: generateLabel(length), + ...propOverrides, +}); + +describe('calculateWidthFromEntries', () => { + it('calculates width for array of strings', () => { + const shortLabels = [10, 20].map(generateLabel); + expect(calculateWidthFromEntries(shortLabels)).toBe(256); + + const mediumLabels = [50, 55, 10, 20].map(generateLabel); + expect(calculateWidthFromEntries(mediumLabels)).toBe(501); + + const longLabels = [80, 90, 10].map(generateLabel); + expect(calculateWidthFromEntries(longLabels)).toBe(MAX_WIDTH); + }); + + it('calculates width for array of objects with keys', () => { + const shortLabels = [10, 20].map((v) => generateObjectWithLabelOfLength(v)); + expect(calculateWidthFromEntries(shortLabels, ['label'])).toBe(256); + + const mediumLabels = [50, 55, 10, 20].map((v) => generateObjectWithLabelOfLength(v)); + expect(calculateWidthFromEntries(mediumLabels, ['label'])).toBe(501); + + const longLabels = [80, 90, 10].map((v) => generateObjectWithLabelOfLength(v)); + expect(calculateWidthFromEntries(longLabels, ['label'])).toBe(MAX_WIDTH); + }); + it('calculates width for array of objects for fallback keys', () => { + const shortLabels = [10, 20].map((v) => + generateObjectWithLabelOfLength(v, { label: undefined, name: generateLabel(v) }) + ); + expect(calculateWidthFromEntries(shortLabels, ['id', 'label', 'name'])).toBe(256); + + const mediumLabels = [50, 55, 10, 20].map((v) => + generateObjectWithLabelOfLength(v, { label: undefined, name: generateLabel(v) }) + ); + expect(calculateWidthFromEntries(mediumLabels, ['id', 'label', 'name'])).toBe(501); + }); +}); diff --git a/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_entries.ts b/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_entries.ts new file mode 100644 index 0000000000000..4a6795c8ea077 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_entries.ts @@ -0,0 +1,39 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { LIMITS, calculateWidthFromCharCount } from './calculate_width_from_char_count'; + +type GenericObject> = T; + +const getMaxLabelLengthForObjects = ( + entries: GenericObject[], + labelKeys: Array +) => + entries.reduce((acc, curr) => { + const labelKey = labelKeys.find((key) => curr[key]); + if (!labelKey) { + return acc; + } + const labelLength = curr[labelKey].length; + return acc > labelLength ? acc : labelLength; + }, 0); + +const getMaxLabelLengthForStrings = (arr: string[]) => + arr.reduce((acc, curr) => (acc > curr.length ? acc : curr.length), 0); + +export function calculateWidthFromEntries( + entries: GenericObject[] | string[], + labelKeys?: Array, + overridesPanelWidths?: Partial +) { + const maxLabelLength = labelKeys + ? getMaxLabelLengthForObjects(entries as GenericObject[], labelKeys) + : getMaxLabelLengthForStrings(entries as string[]); + + return calculateWidthFromCharCount(maxLabelLength, overridesPanelWidths); +} diff --git a/packages/kbn-calculate-width-from-char-count/src/index.ts b/packages/kbn-calculate-width-from-char-count/src/index.ts new file mode 100644 index 0000000000000..33fcddecf7403 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/src/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { calculateWidthFromCharCount } from './calculate_width_from_char_count'; + +export { calculateWidthFromEntries } from './calculate_width_from_entries'; diff --git a/packages/kbn-calculate-width-from-char-count/tsconfig.json b/packages/kbn-calculate-width-from-char-count/tsconfig.json new file mode 100644 index 0000000000000..ea0a30fa75171 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react", + ], + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "kbn_references": [], + "exclude": [ + "target/**/*", + ] +} diff --git a/packages/kbn-visualization-ui-components/components/field_picker/field_picker.test.tsx b/packages/kbn-visualization-ui-components/components/field_picker/field_picker.test.tsx new file mode 100644 index 0000000000000..1b821dd44bc93 --- /dev/null +++ b/packages/kbn-visualization-ui-components/components/field_picker/field_picker.test.tsx @@ -0,0 +1,89 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { FieldPicker, FieldPickerProps } from './field_picker'; +import { render, screen } from '@testing-library/react'; +import faker from 'faker'; +import userEvent from '@testing-library/user-event'; +import { DataType, FieldOptionValue } from './types'; + +const generateFieldWithLabelOfLength = (length: number) => ({ + label: faker.random.alpha({ count: length }), + value: { + type: 'field' as const, + field: faker.random.alpha({ count: length }), + dataType: 'date' as DataType, + operationType: 'count', + }, + exists: true, + compatible: 1, +}); + +const generateProps = (customField = generateFieldWithLabelOfLength(20)) => + ({ + selectedOptions: [ + { + label: 'Category', + value: { + type: 'field' as const, + field: 'category.keyword', + dataType: 'keyword' as DataType, + operationType: 'count', + }, + }, + ], + options: [ + { + label: 'nested options', + exists: true, + compatible: 1, + value: generateFieldWithLabelOfLength(20), + options: [ + generateFieldWithLabelOfLength(20), + customField, + generateFieldWithLabelOfLength(20), + ], + }, + ], + onChoose: jest.fn(), + fieldIsInvalid: false, + } as unknown as FieldPickerProps); + +describe('field picker', () => { + const renderFieldPicker = (customField = generateFieldWithLabelOfLength(20)) => { + const props = generateProps(customField); + const rtlRender = render(); + return { + openCombobox: () => userEvent.click(screen.getByLabelText(/open list of options/i)), + ...rtlRender, + }; + }; + + it('should render minimum width dropdown list if all labels are short', async () => { + const { openCombobox } = renderFieldPicker(); + openCombobox(); + const popover = screen.getByRole('dialog'); + expect(popover).toHaveStyle('inline-size: 256px'); + }); + + it('should render calculated width dropdown list if the longest label is longer than min width', async () => { + const { openCombobox } = renderFieldPicker(generateFieldWithLabelOfLength(50)); + openCombobox(); + + const popover = screen.getByRole('dialog'); + expect(popover).toHaveStyle('inline-size: 466px'); + }); + + it('should render maximum width dropdown list if the longest label is longer than max width', async () => { + const { openCombobox } = renderFieldPicker(generateFieldWithLabelOfLength(80)); + openCombobox(); + const popover = screen.getByRole('dialog'); + expect(popover).toHaveStyle('inline-size: 550px'); + }); +}); diff --git a/packages/kbn-visualization-ui-components/components/field_picker/field_picker.tsx b/packages/kbn-visualization-ui-components/components/field_picker/field_picker.tsx index 5b6022d5cb454..237b7c85cd8fd 100644 --- a/packages/kbn-visualization-ui-components/components/field_picker/field_picker.tsx +++ b/packages/kbn-visualization-ui-components/components/field_picker/field_picker.tsx @@ -9,9 +9,10 @@ import './field_picker.scss'; import React from 'react'; import { i18n } from '@kbn/i18n'; +import classNames from 'classnames'; import { EuiComboBox, EuiComboBoxProps } from '@elastic/eui'; import { FieldIcon } from '@kbn/field-utils/src/components/field_icon'; -import classNames from 'classnames'; +import { calculateWidthFromCharCount } from '@kbn/calculate-width-from-char-count'; import type { FieldOptionValue, FieldOption } from './types'; export interface FieldPickerProps @@ -27,23 +28,26 @@ export interface FieldPickerProps const MIDDLE_TRUNCATION_PROPS = { truncation: 'middle' as const }; const SINGLE_SELECTION_AS_TEXT_PROPS = { asPlainText: true }; -export function FieldPicker({ - selectedOptions, - options, - onChoose, - onDelete, - fieldIsInvalid, - ['data-test-subj']: dataTestSub, - ...rest -}: FieldPickerProps) { - let theLongestLabel = ''; +export function FieldPicker( + props: FieldPickerProps +) { + const { + selectedOptions, + options, + onChoose, + onDelete, + fieldIsInvalid, + ['data-test-subj']: dataTestSub, + ...rest + } = props; + let maxLabelLength = 0; const styledOptions = options?.map(({ compatible, exists, ...otherAttr }) => { if (otherAttr.options) { return { ...otherAttr, options: otherAttr.options.map(({ exists: fieldOptionExists, ...fieldOption }) => { - if (fieldOption.label.length > theLongestLabel.length) { - theLongestLabel = fieldOption.label; + if (fieldOption.label.length > maxLabelLength) { + maxLabelLength = fieldOption.label.length; } return { ...fieldOption, @@ -75,7 +79,6 @@ export function FieldPicker({ }; }); - const panelMinWidth = getPanelMinWidth(theLongestLabel.length); return ( ({ selectedOptions={selectedOptions} singleSelection={SINGLE_SELECTION_AS_TEXT_PROPS} truncationProps={MIDDLE_TRUNCATION_PROPS} - inputPopoverProps={{ panelMinWidth }} + inputPopoverProps={{ + panelMinWidth: calculateWidthFromCharCount(maxLabelLength), + anchorPosition: 'downRight', + }} onChange={(choices) => { if (choices.length === 0) { onDelete?.(); @@ -102,20 +108,3 @@ export function FieldPicker({ /> ); } - -const MINIMUM_POPOVER_WIDTH = 300; -const MINIMUM_POPOVER_WIDTH_CHAR_COUNT = 28; -const AVERAGE_CHAR_WIDTH = 7; -const MAXIMUM_POPOVER_WIDTH_CHAR_COUNT = 60; -const MAXIMUM_POPOVER_WIDTH = 550; // fitting 60 characters - -function getPanelMinWidth(labelLength: number) { - if (labelLength > MAXIMUM_POPOVER_WIDTH_CHAR_COUNT) { - return MAXIMUM_POPOVER_WIDTH; - } - if (labelLength > MINIMUM_POPOVER_WIDTH_CHAR_COUNT) { - const overflownChars = labelLength - MINIMUM_POPOVER_WIDTH_CHAR_COUNT; - return MINIMUM_POPOVER_WIDTH + overflownChars * AVERAGE_CHAR_WIDTH; - } - return MINIMUM_POPOVER_WIDTH; -} diff --git a/packages/kbn-visualization-ui-components/tsconfig.json b/packages/kbn-visualization-ui-components/tsconfig.json index 78f0b8a4b111f..a9d6627828dc7 100644 --- a/packages/kbn-visualization-ui-components/tsconfig.json +++ b/packages/kbn-visualization-ui-components/tsconfig.json @@ -31,5 +31,6 @@ "@kbn/coloring", "@kbn/field-formats-plugin", "@kbn/field-utils", + "@kbn/calculate-width-from-char-count" ], } diff --git a/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx b/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx index 1804a2fcf2046..8e1c7fbc74b99 100644 --- a/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx +++ b/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx @@ -9,6 +9,7 @@ import React, { useState } from 'react'; import { EuiSelectable, EuiInputPopover, EuiSelectableProps } from '@elastic/eui'; import { DataViewListItem } from '@kbn/data-views-plugin/common'; +import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count'; import { ToolbarButton, ToolbarButtonProps } from '@kbn/shared-ux-button-toolbar'; @@ -67,6 +68,7 @@ export function DataViewPicker({ isOpen={isPopoverOpen} input={createTrigger()} closePopover={() => setPopoverIsOpen(false)} + panelMinWidth={calculateWidthFromEntries(dataViews, ['name', 'id'])} panelProps={{ 'data-test-subj': 'data-view-picker-popover', }} diff --git a/src/plugins/presentation_util/tsconfig.json b/src/plugins/presentation_util/tsconfig.json index e17fd6cc5a754..4076319587e17 100644 --- a/src/plugins/presentation_util/tsconfig.json +++ b/src/plugins/presentation_util/tsconfig.json @@ -31,7 +31,8 @@ "@kbn/ui-actions-plugin", "@kbn/saved-objects-finder-plugin", "@kbn/content-management-plugin", - "@kbn/shared-ux-button-toolbar" + "@kbn/shared-ux-button-toolbar", + "@kbn/calculate-width-from-char-count" ], "exclude": ["target/**/*"] } diff --git a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx index e3e4059ad3cf5..77e00e157d62b 100644 --- a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx +++ b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx @@ -9,6 +9,7 @@ import { EuiComboBox, EuiComboBoxOptionOption, EuiToolTip, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; import { DataView, DataViewField } from '@kbn/data-views-plugin/common'; +import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count'; import { i18n } from '@kbn/i18n'; import React, { useCallback, useState } from 'react'; import { UnifiedHistogramBreakdownContext } from '../types'; @@ -59,11 +60,10 @@ export const BreakdownFieldSelector = ({ const breakdownCss = css` width: 100%; max-width: ${euiTheme.base * 22}px; - &:focus-within { - max-width: ${euiTheme.base * 30}px; - } `; + const panelMinWidth = calculateWidthFromEntries(fieldOptions, ['label']); + return ( { +const MIN_WIDTH = 300; + +export const changeDataViewStyles = ({ + fullWidth, + dataViewsList, +}: { + fullWidth?: boolean; + dataViewsList: DataViewListItemEnhanced[]; +}) => { return { trigger: { - maxWidth: fullWidth ? undefined : DATA_VIEW_POPOVER_CONTENT_WIDTH, + maxWidth: fullWidth ? undefined : MIN_WIDTH, }, popoverContent: { - width: DATA_VIEW_POPOVER_CONTENT_WIDTH, + width: calculateWidthFromEntries(dataViewsList, ['name', 'id'], { minWidth: MIN_WIDTH }), }, }; }; diff --git a/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx b/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx index 8c35ed21568bb..1398483fa0a1a 100644 --- a/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx +++ b/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx @@ -96,7 +96,9 @@ export function ChangeDataView({ const { application, data, storage, dataViews, dataViewEditor, appName, usageCollection } = kibana.services; const reportUiCounter = usageCollection?.reportUiCounter.bind(usageCollection, appName); - const styles = changeDataViewStyles({ fullWidth: trigger.fullWidth }); + + const styles = changeDataViewStyles({ fullWidth: trigger.fullWidth, dataViewsList }); + const [isTextLangTransitionModalDismissed, setIsTextLangTransitionModalDismissed] = useState(() => Boolean(storage.get(TEXT_LANG_TRANSITION_MODAL_KEY)) ); diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_value_input.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_value_input.tsx index adb80df6cf543..9328ecfa66c50 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_value_input.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_value_input.tsx @@ -10,6 +10,7 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n-react'; import { uniq } from 'lodash'; import React from 'react'; import { withKibana } from '@kbn/kibana-react-plugin/public'; +import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count'; import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor'; import { ValueInputType } from './value_input_type'; @@ -26,7 +27,6 @@ interface PhraseValueInputProps extends PhraseSuggestorProps { } class PhraseValueInputUI extends PhraseSuggestorUI { - comboBoxWrapperRef = React.createRef(); inputRef: HTMLInputElement | null = null; public render() { @@ -59,43 +59,39 @@ class PhraseValueInputUI extends PhraseSuggestorUI { // there are cases when the value is a number, this would cause an exception const valueAsStr = String(value); const options = value ? uniq([valueAsStr, ...suggestions]) : suggestions; + const panelMinWidth = calculateWidthFromEntries(options); return ( -
- { - this.inputRef = ref; - }} - isDisabled={this.props.disabled} - fullWidth={fullWidth} - compressed={this.props.compressed} - placeholder={intl.formatMessage({ - id: 'unifiedSearch.filter.filterEditor.valueSelectPlaceholder', - defaultMessage: 'Select a value', - })} - aria-label={intl.formatMessage({ - id: 'unifiedSearch.filter.filterEditor.valueSelectPlaceholder', - defaultMessage: 'Select a value', - })} - options={options} - getLabel={(option) => option} - selectedOptions={value ? [valueAsStr] : []} - onChange={([newValue = '']) => { - onChange(newValue); - setTimeout(() => { - // Note: requires a tick skip to correctly blur element focus - this.inputRef?.blur(); - }); - }} - onSearchChange={this.onSearchChange} - onCreateOption={onChange} - isClearable={false} - data-test-subj="filterParamsComboBox phraseParamsComboxBox" - singleSelection={SINGLE_SELECTION_AS_TEXT_PROPS} - truncationProps={MIDDLE_TRUNCATION_PROPS} - /> -
+ { + this.inputRef = ref; + }} + isDisabled={this.props.disabled} + fullWidth={fullWidth} + compressed={this.props.compressed} + placeholder={intl.formatMessage({ + id: 'unifiedSearch.filter.filterEditor.valueSelectPlaceholder', + defaultMessage: 'Select a value', + })} + aria-label={intl.formatMessage({ + id: 'unifiedSearch.filter.filterEditor.valueSelectPlaceholder', + defaultMessage: 'Select a value', + })} + options={options} + getLabel={(option) => option} + selectedOptions={value ? [valueAsStr] : []} + onChange={([newValue = '']) => { + onChange(newValue); + }} + onSearchChange={this.onSearchChange} + onCreateOption={onChange} + isClearable={false} + data-test-subj="filterParamsComboBox phraseParamsComboxBox" + singleSelection={SINGLE_SELECTION_AS_TEXT_PROPS} + truncationProps={MIDDLE_TRUNCATION_PROPS} + inputPopoverProps={{ panelMinWidth, anchorPosition: 'downRight' }} + /> ); } } diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/phrases_values_input.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/phrases_values_input.tsx index 500b875f42667..30fd03fb3d9c2 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/phrases_values_input.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/phrases_values_input.tsx @@ -11,6 +11,7 @@ import { uniq } from 'lodash'; import React from 'react'; import { withKibana } from '@kbn/kibana-react-plugin/public'; import { withEuiTheme, WithEuiThemeProps } from '@elastic/eui'; +import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count'; import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor'; import { phrasesValuesComboboxCss } from './phrases_values_input.styles'; @@ -28,45 +29,42 @@ interface Props { export type PhrasesValuesInputProps = Props & PhraseSuggestorProps & WithEuiThemeProps; class PhrasesValuesInputUI extends PhraseSuggestorUI { - comboBoxWrapperRef = React.createRef(); - public render() { const { suggestions, isLoading } = this.state; const { values, intl, onChange, fullWidth, onParamsUpdate, compressed, disabled } = this.props; const options = values ? uniq([...values, ...suggestions]) : suggestions; - + const panelMinWidth = calculateWidthFromEntries(options); return ( -
- option} - selectedOptions={values || []} - onSearchChange={this.onSearchChange} - onCreateOption={(option: string) => { - onParamsUpdate(option.trim()); - }} - className={phrasesValuesComboboxCss(this.props.theme)} - onChange={onChange} - isClearable={false} - data-test-subj="filterParamsComboBox phrasesParamsComboxBox" - isDisabled={disabled} - truncationProps={MIDDLE_TRUNCATION_PROPS} - /> -
+ option} + selectedOptions={values || []} + onSearchChange={this.onSearchChange} + onCreateOption={(option: string) => { + onParamsUpdate(option.trim()); + }} + className={phrasesValuesComboboxCss(this.props.theme)} + onChange={onChange} + isClearable={false} + data-test-subj="filterParamsComboBox phrasesParamsComboxBox" + isDisabled={disabled} + truncationProps={MIDDLE_TRUNCATION_PROPS} + inputPopoverProps={{ panelMinWidth, anchorPosition: 'downRight' }} + /> ); } } diff --git a/src/plugins/unified_search/public/filters_builder/filter_item/field_input.tsx b/src/plugins/unified_search/public/filters_builder/filter_item/field_input.tsx index 540226caef525..cc87c3de78936 100644 --- a/src/plugins/unified_search/public/filters_builder/filter_item/field_input.tsx +++ b/src/plugins/unified_search/public/filters_builder/filter_item/field_input.tsx @@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n'; import { FieldIcon } from '@kbn/react-field'; import { KBN_FIELD_TYPES } from '@kbn/field-types'; import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; +import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count'; import { useGeneratedHtmlId, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { getFilterableFields } from '../../filter_bar/filter_editor'; import { FiltersBuilderContextType } from '../context'; @@ -36,7 +37,6 @@ export function FieldInput({ field, dataView, onHandleField }: FieldInputProps) const { disabled, suggestionsAbstraction } = useContext(FiltersBuilderContextType); const fields = dataView ? getFilterableFields(dataView) : []; const id = useGeneratedHtmlId({ prefix: 'fieldInput' }); - const comboBoxWrapperRef = useRef(null); const inputRef = useRef(null); const onFieldChange = useCallback( @@ -72,40 +72,30 @@ export function FieldInput({ field, dataView, onHandleField }: FieldInputProps) ({ label }) => fields[optionFields.findIndex((optionField) => optionField.label === label)] ); onFieldChange(newValues); - - setTimeout(() => { - // Note: requires a tick skip to correctly blur element focus - inputRef?.current?.blur(); - }); }; - const handleFocus: React.FocusEventHandler = () => { - // Force focus on input due to https://github.com/elastic/eui/issues/7170 - inputRef?.current?.focus(); - }; + const panelMinWidth = calculateWidthFromEntries(euiOptions, ['label']); return ( -
- { - inputRef.current = ref; - }} - options={euiOptions} - selectedOptions={selectedEuiOptions} - onChange={onComboBoxChange} - isDisabled={disabled} - placeholder={strings.getFieldSelectPlaceholderLabel()} - sortMatchesBy="startsWith" - aria-label={strings.getFieldSelectPlaceholderLabel()} - isClearable={false} - compressed - fullWidth - onFocus={handleFocus} - data-test-subj="filterFieldSuggestionList" - singleSelection={SINGLE_SELECTION_AS_TEXT_PROPS} - truncationProps={MIDDLE_TRUNCATION_PROPS} - /> -
+ { + inputRef.current = ref; + }} + options={euiOptions} + selectedOptions={selectedEuiOptions} + onChange={onComboBoxChange} + isDisabled={disabled} + placeholder={strings.getFieldSelectPlaceholderLabel()} + sortMatchesBy="startsWith" + aria-label={strings.getFieldSelectPlaceholderLabel()} + isClearable={false} + compressed + fullWidth + data-test-subj="filterFieldSuggestionList" + singleSelection={SINGLE_SELECTION_AS_TEXT_PROPS} + truncationProps={MIDDLE_TRUNCATION_PROPS} + inputPopoverProps={{ panelMinWidth }} + /> ); } diff --git a/src/plugins/unified_search/public/filters_builder/filter_item/filter_item.styles.ts b/src/plugins/unified_search/public/filters_builder/filter_item/filter_item.styles.ts index 6ec0ac9ab7058..78c4952aa69b0 100644 --- a/src/plugins/unified_search/public/filters_builder/filter_item/filter_item.styles.ts +++ b/src/plugins/unified_search/public/filters_builder/filter_item/filter_item.styles.ts @@ -26,9 +26,6 @@ export const fieldAndParamCss = (euiTheme: EuiThemeComputed) => css` .euiFormRow { max-width: 800px; } - &:focus-within { - flex-grow: 4; - } `; export const operationCss = (euiTheme: EuiThemeComputed) => css` diff --git a/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx b/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx index f7148db93ce19..d8517eedba4ed 100644 --- a/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx +++ b/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx @@ -11,7 +11,9 @@ import React, { Component } from 'react'; import { Required } from '@kbn/utility-types'; import { EuiComboBox, EuiComboBoxProps } from '@elastic/eui'; +import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count'; import type { DataViewsContract } from '@kbn/data-views-plugin/public'; +import { MIDDLE_TRUNCATION_PROPS } from '../filter_bar/filter_editor/lib/helpers'; export type IndexPatternSelectProps = Required< Omit, 'onSearchChange' | 'options' | 'selectedOptions' | 'onChange'>, @@ -28,7 +30,7 @@ export type IndexPatternSelectInternalProps = IndexPatternSelectProps & { interface IndexPatternSelectState { isLoading: boolean; - options: []; + options: Array<{ value: string; label: string }>; selectedIndexPattern: { value: string; label: string } | undefined; searchValue: string | undefined; } @@ -147,6 +149,8 @@ export default class IndexPatternSelect extends Component ); } diff --git a/src/plugins/unified_search/tsconfig.json b/src/plugins/unified_search/tsconfig.json index f83de4ff80fc7..0412bbc4c8c98 100644 --- a/src/plugins/unified_search/tsconfig.json +++ b/src/plugins/unified_search/tsconfig.json @@ -42,6 +42,7 @@ "@kbn/core-doc-links-browser", "@kbn/core-lifecycle-browser", "@kbn/ml-string-hash", + "@kbn/calculate-width-from-char-count" ], "exclude": [ "target/**/*", diff --git a/tsconfig.base.json b/tsconfig.base.json index 0f4d5c25aba60..3c4a87242841b 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -104,6 +104,8 @@ "@kbn/bfetch-plugin/*": ["src/plugins/bfetch/*"], "@kbn/calculate-auto": ["packages/kbn-calculate-auto"], "@kbn/calculate-auto/*": ["packages/kbn-calculate-auto/*"], + "@kbn/calculate-width-from-char-count": ["packages/kbn-calculate-width-from-char-count"], + "@kbn/calculate-width-from-char-count/*": ["packages/kbn-calculate-width-from-char-count/*"], "@kbn/canvas-plugin": ["x-pack/plugins/canvas"], "@kbn/canvas-plugin/*": ["x-pack/plugins/canvas/*"], "@kbn/cases-api-integration-test-plugin": ["x-pack/test/cases_api_integration/common/plugins/cases"], diff --git a/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx b/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx index 9d55284cc36c8..c4efd626d4772 100644 --- a/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx +++ b/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx @@ -6,9 +6,11 @@ */ import { i18n } from '@kbn/i18n'; +import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count'; import React, { useState } from 'react'; import { EuiPopover, EuiPopoverTitle, EuiSelectableProps } from '@elastic/eui'; import { DataViewsList } from '@kbn/unified-search-plugin/public'; +import { css } from '@emotion/react'; import { type IndexPatternRef } from '../../types'; import { type ChangeIndexPatternTriggerProps, TriggerButton } from './trigger'; @@ -30,43 +32,47 @@ export function ChangeIndexPattern({ const [isPopoverOpen, setPopoverIsOpen] = useState(false); return ( - <> - setPopoverIsOpen(!isPopoverOpen)} - /> - } - panelProps={{ - ['data-test-subj']: 'lnsChangeIndexPatternPopover', - }} - isOpen={isPopoverOpen} - closePopover={() => setPopoverIsOpen(false)} - display="block" - panelPaddingSize="none" - ownFocus + setPopoverIsOpen(!isPopoverOpen)} + /> + } + panelProps={{ + ['data-test-subj']: 'lnsChangeIndexPatternPopover', + }} + isOpen={isPopoverOpen} + closePopover={() => setPopoverIsOpen(false)} + display="block" + panelPaddingSize="none" + ownFocus + > +
-
- - {i18n.translate('xpack.lens.indexPattern.changeDataViewTitle', { - defaultMessage: 'Data view', - })} - + + {i18n.translate('xpack.lens.indexPattern.changeDataViewTitle', { + defaultMessage: 'Data view', + })} + - { - onChangeIndexPattern(newId); - setPopoverIsOpen(false); - }} - currentDataViewId={indexPatternId} - selectableProps={selectableProps} - /> -
- - + { + onChangeIndexPattern(newId); + setPopoverIsOpen(false); + }} + currentDataViewId={indexPatternId} + selectableProps={selectableProps} + /> +
+
); } diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx index 99c7b2bec30d4..30ffc3a32b1f7 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx @@ -170,7 +170,6 @@ function DataLayerHeader(props: VisualizationLayerWidgetProps) { return ( setPopoverIsOpen(!isPopoverOpen)} @@ -188,7 +187,11 @@ function DataLayerHeader(props: VisualizationLayerWidgetProps) { defaultMessage: 'Layer visualization type', })} -
+
Date: Tue, 28 Nov 2023 13:35:43 +0100 Subject: [PATCH 10/14] [ftr] split x-pack accessibility config in 3 groups (#171186) ## Summary Splitting long running FTR config: image After split: | Config Path | Runtime | | ------------- | ------------- | | x-pack/test/accessibility/apps/group1/config.ts | 10m 15s | | x-pack/test/accessibility/apps/group2/config.ts | 14m 31s | | x-pack/test/accessibility/apps/group3/config.ts | 11m 30s | --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .buildkite/ftr_configs.yml | 4 +- .github/CODEOWNERS | 16 ++--- .../development-accessibility-tests.asciidoc | 2 +- .../interpreting-ci-failures.asciidoc | 2 +- x-pack/plugins/enterprise_search/README.md | 2 +- x-pack/plugins/ml/readme.md | 2 +- x-pack/plugins/transform/readme.md | 2 +- .../apps/{ => group1}/advanced_settings.ts | 2 +- .../test/accessibility/apps/group1/config.ts | 29 ++++++++ .../apps/{ => group1}/dashboard_controls.ts | 2 +- .../{ => group1}/dashboard_panel_options.ts | 4 +- .../apps/{ => group1}/grok_debugger.ts | 2 +- .../apps/{ => group1}/helpers.ts | 0 .../accessibility/apps/{ => group1}/home.ts | 2 +- .../test/accessibility/apps/group1/index.ts | 29 ++++++++ .../index_lifecycle_management.ts | 2 +- .../{ => group1}/ingest_node_pipelines.ts | 0 .../apps/{ => group1}/kibana_overview.ts | 2 +- .../apps/{ => group1}/login_page.ts | 2 +- .../apps/{ => group1}/management.ts | 2 +- .../apps/{ => group1}/painless_lab.ts | 2 +- .../accessibility/apps/{ => group1}/roles.ts | 2 +- .../apps/{ => group1}/search_profiler.ts | 2 +- .../accessibility/apps/{ => group1}/spaces.ts | 2 +- .../accessibility/apps/{ => group1}/uptime.ts | 7 +- .../accessibility/apps/{ => group1}/users.ts | 2 +- .../test/accessibility/apps/group2/config.ts | 29 ++++++++ .../test/accessibility/apps/group2/index.ts | 17 +++++ .../accessibility/apps/{ => group2}/lens.ts | 13 ++-- .../accessibility/apps/{ => group2}/ml.ts | 4 +- .../apps/{ => group2}/ml_anomaly_detection.ts | 2 +- .../apps/{ => group2}/transform.ts | 2 +- .../accessibility/apps/{ => group3}/canvas.ts | 2 +- .../test/accessibility/apps/group3/config.ts | 29 ++++++++ .../{ => group3}/cross_cluster_replication.ts | 5 +- .../apps/{ => group3}/enterprise_search.ts | 2 +- .../accessibility/apps/{ => group3}/graph.ts | 2 +- .../apps/group3/grok_debugger.ts | 37 ++++++++++ .../test/accessibility/apps/group3/index.ts | 35 ++++++++++ .../apps/{ => group3}/license_management.ts | 2 +- .../accessibility/apps/{ => group3}/maps.ts | 2 +- .../ml_embeddables_in_dashboard.ts | 2 +- .../apps/{ => group3}/observability.ts | 2 +- .../apps/{ => group3}/remote_clusters.ts | 2 +- .../apps/{ => group3}/reporting.ts | 2 +- .../apps/{ => group3}/rollup_jobs.ts | 2 +- .../apps/{ => group3}/rules_connectors.ts | 2 +- .../apps/{ => group3}/search_sessions.ts | 2 +- .../apps/{ => group3}/security_solution.ts | 2 +- .../apps/{ => group3}/snapshot_and_restore.ts | 2 +- .../apps/{ => group3}/stack_monitoring.ts | 2 +- .../accessibility/apps/{ => group3}/tags.ts | 2 +- .../apps/{ => group3}/upgrade_assistant.ts | 2 +- .../apps/{ => group3}/watcher.ts | 2 +- x-pack/test/accessibility/config.ts | 70 ------------------- 55 files changed, 272 insertions(+), 132 deletions(-) rename x-pack/test/accessibility/apps/{ => group1}/advanced_settings.ts (97%) create mode 100644 x-pack/test/accessibility/apps/group1/config.ts rename x-pack/test/accessibility/apps/{ => group1}/dashboard_controls.ts (98%) rename x-pack/test/accessibility/apps/{ => group1}/dashboard_panel_options.ts (97%) rename x-pack/test/accessibility/apps/{ => group1}/grok_debugger.ts (94%) rename x-pack/test/accessibility/apps/{ => group1}/helpers.ts (100%) rename x-pack/test/accessibility/apps/{ => group1}/home.ts (97%) create mode 100644 x-pack/test/accessibility/apps/group1/index.ts rename x-pack/test/accessibility/apps/{ => group1}/index_lifecycle_management.ts (98%) rename x-pack/test/accessibility/apps/{ => group1}/ingest_node_pipelines.ts (100%) rename x-pack/test/accessibility/apps/{ => group1}/kibana_overview.ts (93%) rename x-pack/test/accessibility/apps/{ => group1}/login_page.ts (97%) rename x-pack/test/accessibility/apps/{ => group1}/management.ts (97%) rename x-pack/test/accessibility/apps/{ => group1}/painless_lab.ts (97%) rename x-pack/test/accessibility/apps/{ => group1}/roles.ts (98%) rename x-pack/test/accessibility/apps/{ => group1}/search_profiler.ts (97%) rename x-pack/test/accessibility/apps/{ => group1}/spaces.ts (98%) rename x-pack/test/accessibility/apps/{ => group1}/uptime.ts (91%) rename x-pack/test/accessibility/apps/{ => group1}/users.ts (98%) create mode 100644 x-pack/test/accessibility/apps/group2/config.ts create mode 100644 x-pack/test/accessibility/apps/group2/index.ts rename x-pack/test/accessibility/apps/{ => group2}/lens.ts (98%) rename x-pack/test/accessibility/apps/{ => group2}/ml.ts (98%) rename x-pack/test/accessibility/apps/{ => group2}/ml_anomaly_detection.ts (99%) rename x-pack/test/accessibility/apps/{ => group2}/transform.ts (99%) rename x-pack/test/accessibility/apps/{ => group3}/canvas.ts (96%) create mode 100644 x-pack/test/accessibility/apps/group3/config.ts rename x-pack/test/accessibility/apps/{ => group3}/cross_cluster_replication.ts (95%) rename x-pack/test/accessibility/apps/{ => group3}/enterprise_search.ts (98%) rename x-pack/test/accessibility/apps/{ => group3}/graph.ts (98%) create mode 100644 x-pack/test/accessibility/apps/group3/grok_debugger.ts create mode 100644 x-pack/test/accessibility/apps/group3/index.ts rename x-pack/test/accessibility/apps/{ => group3}/license_management.ts (95%) rename x-pack/test/accessibility/apps/{ => group3}/maps.ts (98%) rename x-pack/test/accessibility/apps/{ => group3}/ml_embeddables_in_dashboard.ts (98%) rename x-pack/test/accessibility/apps/{ => group3}/observability.ts (95%) rename x-pack/test/accessibility/apps/{ => group3}/remote_clusters.ts (99%) rename x-pack/test/accessibility/apps/{ => group3}/reporting.ts (97%) rename x-pack/test/accessibility/apps/{ => group3}/rollup_jobs.ts (98%) rename x-pack/test/accessibility/apps/{ => group3}/rules_connectors.ts (98%) rename x-pack/test/accessibility/apps/{ => group3}/search_sessions.ts (98%) rename x-pack/test/accessibility/apps/{ => group3}/security_solution.ts (98%) rename x-pack/test/accessibility/apps/{ => group3}/snapshot_and_restore.ts (98%) rename x-pack/test/accessibility/apps/{ => group3}/stack_monitoring.ts (97%) rename x-pack/test/accessibility/apps/{ => group3}/tags.ts (98%) rename x-pack/test/accessibility/apps/{ => group3}/upgrade_assistant.ts (99%) rename x-pack/test/accessibility/apps/{ => group3}/watcher.ts (95%) delete mode 100644 x-pack/test/accessibility/config.ts diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index e4d1f63763031..e51fc01430b8c 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -158,7 +158,9 @@ enabled: - test/server_integration/http/ssl_with_p12/config.js - test/server_integration/http/ssl/config.js - test/ui_capabilities/newsfeed_err/config.ts - - x-pack/test/accessibility/config.ts + - x-pack/test/accessibility/apps/group1/config.ts + - x-pack/test/accessibility/apps/group2/config.ts + - x-pack/test/accessibility/apps/group3/config.ts - x-pack/test/localization/config.ja_jp.ts - x-pack/test/localization/config.fr_fr.ts - x-pack/test/localization/config.zh_cn.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6780cf97c1336..89e152a2fe40f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -872,7 +872,7 @@ packages/kbn-zod-helpers @elastic/security-detection-rule-management /test/functional/apps/management/ccs_compatibility/_data_views_ccs.ts @elastic/kibana-data-discovery /test/functional/apps/management/data_views @elastic/kibana-data-discovery /test/plugin_functional/test_suites/data_plugin @elastic/kibana-data-discovery -/x-pack/test/accessibility/apps/search_sessions.ts @elastic/kibana-data-discovery +/x-pack/test/accessibility/apps/group3/search_sessions.ts @elastic/kibana-data-discovery /x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js @elastic/kibana-data-discovery /x-pack/test/api_integration/apis/search @elastic/kibana-data-discovery /x-pack/test/examples/search_examples @elastic/kibana-data-discovery @@ -1075,8 +1075,8 @@ x-pack/plugins/infra/server/lib/alerting @elastic/obs-ux-management-team #CC# /src/plugins/kibana_react/public/code_editor/ @elastic/kibana-presentation # Machine Learning -/x-pack/test/accessibility/apps/ml.ts @elastic/ml-ui -/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts @elastic/ml-ui +/x-pack/test/accessibility/apps/group2/ml.ts @elastic/ml-ui +/x-pack/test/accessibility/apps/group3/ml_embeddables_in_dashboard.ts @elastic/ml-ui /x-pack/test/api_integration/apis/ml/ @elastic/ml-ui /x-pack/test/api_integration_basic/apis/ml/ @elastic/ml-ui /x-pack/test/functional/apps/ml/ @elastic/ml-ui @@ -1090,7 +1090,7 @@ x-pack/plugins/infra/server/lib/alerting @elastic/obs-ux-management-team /x-pack/test/screenshot_creation/services/ml_screenshots.ts @elastic/ml-ui # Additional plugins and packages maintained by the ML team. -/x-pack/test/accessibility/apps/transform.ts @elastic/ml-ui +/x-pack/test/accessibility/apps/group2/transform.ts @elastic/ml-ui /x-pack/test/api_integration/apis/aiops/ @elastic/ml-ui /x-pack/test/api_integration/apis/transform/ @elastic/ml-ui /x-pack/test/api_integration_basic/apis/transform/ @elastic/ml-ui @@ -1177,10 +1177,10 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib /test/interactive_setup_api_integration/ @elastic/kibana-security /test/interactive_setup_functional/ @elastic/kibana-security /test/plugin_functional/test_suites/core_plugins/rendering.ts @elastic/kibana-security -/x-pack/test/accessibility/apps/login_page.ts @elastic/kibana-security -/x-pack/test/accessibility/apps/roles.ts @elastic/kibana-security -/x-pack/test/accessibility/apps/spaces.ts @elastic/kibana-security -/x-pack/test/accessibility/apps/users.ts @elastic/kibana-security +/x-pack/test/accessibility/apps/group1/login_page.ts @elastic/kibana-security +/x-pack/test/accessibility/apps/group1/roles.ts @elastic/kibana-security +/x-pack/test/accessibility/apps/group1/spaces.ts @elastic/kibana-security +/x-pack/test/accessibility/apps/group1/users.ts @elastic/kibana-security /x-pack/test/api_integration/apis/security/ @elastic/kibana-security /x-pack/test/api_integration/apis/spaces/ @elastic/kibana-security /x-pack/test/ui_capabilities/ @elastic/kibana-security diff --git a/docs/developer/contributing/development-accessibility-tests.asciidoc b/docs/developer/contributing/development-accessibility-tests.asciidoc index 2fe2682a3e365..491c16b8a82db 100644 --- a/docs/developer/contributing/development-accessibility-tests.asciidoc +++ b/docs/developer/contributing/development-accessibility-tests.asciidoc @@ -68,7 +68,7 @@ node scripts/functional_test_runner.js --config test/accessibility/config.ts ----------- To run the x-pack tests, swap the config file out for -`x-pack/test/accessibility/config.ts`. +`x-pack/test/accessibility/apps/{group1,group2,group3}/config.ts`. The testing is done using https://github.com/dequelabs/axe-core[axe]. You can run the same thing that runs CI using browser plugins: diff --git a/docs/developer/contributing/interpreting-ci-failures.asciidoc b/docs/developer/contributing/interpreting-ci-failures.asciidoc index 7708c866c3a81..976b3aded3653 100644 --- a/docs/developer/contributing/interpreting-ci-failures.asciidoc +++ b/docs/developer/contributing/interpreting-ci-failures.asciidoc @@ -33,7 +33,7 @@ image::images/test_results.png[Buildkite build screenshot] Looking at the failure, we first look at the Error and stack trace. In the example below, this test failed to find an element within the timeout; `Error: retry.try timeout: TimeoutError: Waiting for element to be located By(css selector, [data-test-subj="createSpace"])` -We know the test file from the stack trace was on line 50 of `test/accessibility/apps/spaces.ts` (this test and the stack trace context is kibana/x-pack/ so the file is https://github.com/elastic/kibana/blob/main/x-pack/test/accessibility/apps/spaces.ts#L50). +We know the test file from the stack trace was on line 50 of `test/accessibility/apps/spaces.ts` (this test and the stack trace context is kibana/x-pack/ so the file is https://github.com/elastic/kibana/blob/main/x-pack/test/accessibility/apps/group1/spaces.ts#L50). The function to click on the element was called from a page object method in `test/functional/page_objects/space_selector_page.ts` https://github.com/elastic/kibana/blob/main/x-pack/test/functional/page_objects/space_selector_page.ts#L58 diff --git a/x-pack/plugins/enterprise_search/README.md b/x-pack/plugins/enterprise_search/README.md index af0cdd43d97b8..bc49c47fe6880 100644 --- a/x-pack/plugins/enterprise_search/README.md +++ b/x-pack/plugins/enterprise_search/README.md @@ -137,4 +137,4 @@ To track what Cypress is doing while running tests, you can pass in `--config vi See [our functional test runner README](../../test/functional_enterprise_search). -Our automated accessibility tests can be found in [x-pack/test/accessibility/apps](../../test/accessibility/apps/enterprise_search.ts). \ No newline at end of file +Our automated accessibility tests can be found in [x-pack/test/accessibility/apps](../../test/accessibility/apps/group3/enterprise_search.ts). \ No newline at end of file diff --git a/x-pack/plugins/ml/readme.md b/x-pack/plugins/ml/readme.md index 72739cc79fffe..235cc4e0458bf 100644 --- a/x-pack/plugins/ml/readme.md +++ b/x-pack/plugins/ml/readme.md @@ -149,7 +149,7 @@ With PATH_TO_CONFIG and other options as follows. - PATH_TO_CONFIG: `test/accessibility/config.ts` - Add `--grep=ml` to the test runner command - - Tests are located in `x-pack/test/accessibility/apps` + - Tests are located in `x-pack/test/accessibility/apps/group2` ## Generating docs screenshots diff --git a/x-pack/plugins/transform/readme.md b/x-pack/plugins/transform/readme.md index 8c25f2ddd8ac4..e86d92340bf0c 100644 --- a/x-pack/plugins/transform/readme.md +++ b/x-pack/plugins/transform/readme.md @@ -151,4 +151,4 @@ With PATH_TO_CONFIG and other options as follows. node scripts/functional_tests_server --config test/accessibility/config.ts node scripts/functional_test_runner.js --config test/accessibility/config.ts --grep=transform - Transform accessibility tests are located in `x-pack/test/accessibility/apps`. + Transform accessibility tests are located in `x-pack/test/accessibility/apps/group2`. diff --git a/x-pack/test/accessibility/apps/advanced_settings.ts b/x-pack/test/accessibility/apps/group1/advanced_settings.ts similarity index 97% rename from x-pack/test/accessibility/apps/advanced_settings.ts rename to x-pack/test/accessibility/apps/group1/advanced_settings.ts index 6c931f0a0e5a1..44899932302ba 100644 --- a/x-pack/test/accessibility/apps/advanced_settings.ts +++ b/x-pack/test/accessibility/apps/group1/advanced_settings.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'settings', 'header']); diff --git a/x-pack/test/accessibility/apps/group1/config.ts b/x-pack/test/accessibility/apps/group1/config.ts new file mode 100644 index 0000000000000..8e5510141abf9 --- /dev/null +++ b/x-pack/test/accessibility/apps/group1/config.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; +import { services } from '../../services'; +import { pageObjects } from '../../page_objects'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile( + require.resolve('../../../functional/config.base.js') + ); + + return { + ...functionalConfig.getAll(), + + testFiles: [require.resolve('.')], + + pageObjects, + services, + + junit: { + reportName: 'X-Pack Accessibility Tests - Group 1', + }, + }; +} diff --git a/x-pack/test/accessibility/apps/dashboard_controls.ts b/x-pack/test/accessibility/apps/group1/dashboard_controls.ts similarity index 98% rename from x-pack/test/accessibility/apps/dashboard_controls.ts rename to x-pack/test/accessibility/apps/group1/dashboard_controls.ts index 74f7288ccce9e..dd45b68674343 100644 --- a/x-pack/test/accessibility/apps/dashboard_controls.ts +++ b/x-pack/test/accessibility/apps/group1/dashboard_controls.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); diff --git a/x-pack/test/accessibility/apps/dashboard_panel_options.ts b/x-pack/test/accessibility/apps/group1/dashboard_panel_options.ts similarity index 97% rename from x-pack/test/accessibility/apps/dashboard_panel_options.ts rename to x-pack/test/accessibility/apps/group1/dashboard_panel_options.ts index 4e4dc3b218d79..5f12f3600c29a 100644 --- a/x-pack/test/accessibility/apps/dashboard_panel_options.ts +++ b/x-pack/test/accessibility/apps/group1/dashboard_panel_options.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; -import { WebElementWrapper } from '../../../../test/functional/services/lib/web_element_wrapper'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import type { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); diff --git a/x-pack/test/accessibility/apps/grok_debugger.ts b/x-pack/test/accessibility/apps/group1/grok_debugger.ts similarity index 94% rename from x-pack/test/accessibility/apps/grok_debugger.ts rename to x-pack/test/accessibility/apps/group1/grok_debugger.ts index 8ee9114c7da0a..da630c6bed7b3 100644 --- a/x-pack/test/accessibility/apps/grok_debugger.ts +++ b/x-pack/test/accessibility/apps/group1/grok_debugger.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'security']); diff --git a/x-pack/test/accessibility/apps/helpers.ts b/x-pack/test/accessibility/apps/group1/helpers.ts similarity index 100% rename from x-pack/test/accessibility/apps/helpers.ts rename to x-pack/test/accessibility/apps/group1/helpers.ts diff --git a/x-pack/test/accessibility/apps/home.ts b/x-pack/test/accessibility/apps/group1/home.ts similarity index 97% rename from x-pack/test/accessibility/apps/home.ts rename to x-pack/test/accessibility/apps/group1/home.ts index 544a32843f7f3..800312bb4de5f 100644 --- a/x-pack/test/accessibility/apps/home.ts +++ b/x-pack/test/accessibility/apps/group1/home.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const { common, home } = getPageObjects(['common', 'home']); diff --git a/x-pack/test/accessibility/apps/group1/index.ts b/x-pack/test/accessibility/apps/group1/index.ts new file mode 100644 index 0000000000000..d8cd2e76c42dd --- /dev/null +++ b/x-pack/test/accessibility/apps/group1/index.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('X-Pack Accessibility Tests - Group 1', function () { + loadTestFile(require.resolve('./login_page')); + loadTestFile(require.resolve('./kibana_overview')); + loadTestFile(require.resolve('./home')); + loadTestFile(require.resolve('./management')); + loadTestFile(require.resolve('./grok_debugger')); + loadTestFile(require.resolve('./search_profiler')); + loadTestFile(require.resolve('./painless_lab')); + loadTestFile(require.resolve('./uptime')); + loadTestFile(require.resolve('./spaces')); + loadTestFile(require.resolve('./advanced_settings')); + loadTestFile(require.resolve('./dashboard_panel_options')); + loadTestFile(require.resolve('./dashboard_controls')); + loadTestFile(require.resolve('./users')); + loadTestFile(require.resolve('./roles')); + loadTestFile(require.resolve('./ingest_node_pipelines')); + loadTestFile(require.resolve('./index_lifecycle_management')); + }); +}; diff --git a/x-pack/test/accessibility/apps/index_lifecycle_management.ts b/x-pack/test/accessibility/apps/group1/index_lifecycle_management.ts similarity index 98% rename from x-pack/test/accessibility/apps/index_lifecycle_management.ts rename to x-pack/test/accessibility/apps/group1/index_lifecycle_management.ts index fc3ec1ff5cf81..b994c4193e49f 100644 --- a/x-pack/test/accessibility/apps/index_lifecycle_management.ts +++ b/x-pack/test/accessibility/apps/group1/index_lifecycle_management.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; const REPO_NAME = 'test'; const POLICY_NAME = 'ilm-a11y-test'; diff --git a/x-pack/test/accessibility/apps/ingest_node_pipelines.ts b/x-pack/test/accessibility/apps/group1/ingest_node_pipelines.ts similarity index 100% rename from x-pack/test/accessibility/apps/ingest_node_pipelines.ts rename to x-pack/test/accessibility/apps/group1/ingest_node_pipelines.ts diff --git a/x-pack/test/accessibility/apps/kibana_overview.ts b/x-pack/test/accessibility/apps/group1/kibana_overview.ts similarity index 93% rename from x-pack/test/accessibility/apps/kibana_overview.ts rename to x-pack/test/accessibility/apps/group1/kibana_overview.ts index 373044c4bffc3..fe1bc55cd4c00 100644 --- a/x-pack/test/accessibility/apps/kibana_overview.ts +++ b/x-pack/test/accessibility/apps/group1/kibana_overview.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'home']); diff --git a/x-pack/test/accessibility/apps/login_page.ts b/x-pack/test/accessibility/apps/group1/login_page.ts similarity index 97% rename from x-pack/test/accessibility/apps/login_page.ts rename to x-pack/test/accessibility/apps/group1/login_page.ts index 3993d9ffcd72e..32cb825f86b33 100644 --- a/x-pack/test/accessibility/apps/login_page.ts +++ b/x-pack/test/accessibility/apps/group1/login_page.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); diff --git a/x-pack/test/accessibility/apps/management.ts b/x-pack/test/accessibility/apps/group1/management.ts similarity index 97% rename from x-pack/test/accessibility/apps/management.ts rename to x-pack/test/accessibility/apps/group1/management.ts index 2021642c2aa27..82c7baf8e830d 100644 --- a/x-pack/test/accessibility/apps/management.ts +++ b/x-pack/test/accessibility/apps/group1/management.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects([ diff --git a/x-pack/test/accessibility/apps/painless_lab.ts b/x-pack/test/accessibility/apps/group1/painless_lab.ts similarity index 97% rename from x-pack/test/accessibility/apps/painless_lab.ts rename to x-pack/test/accessibility/apps/group1/painless_lab.ts index a0a4712dbe4e3..522ffa9c7b238 100644 --- a/x-pack/test/accessibility/apps/painless_lab.ts +++ b/x-pack/test/accessibility/apps/group1/painless_lab.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'security']); diff --git a/x-pack/test/accessibility/apps/roles.ts b/x-pack/test/accessibility/apps/group1/roles.ts similarity index 98% rename from x-pack/test/accessibility/apps/roles.ts rename to x-pack/test/accessibility/apps/group1/roles.ts index 5369dced427fa..cf798bcb853f5 100644 --- a/x-pack/test/accessibility/apps/roles.ts +++ b/x-pack/test/accessibility/apps/group1/roles.ts @@ -7,7 +7,7 @@ // a11y tests for spaces, space selection and spacce creation and feature controls -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['security', 'settings']); diff --git a/x-pack/test/accessibility/apps/search_profiler.ts b/x-pack/test/accessibility/apps/group1/search_profiler.ts similarity index 97% rename from x-pack/test/accessibility/apps/search_profiler.ts rename to x-pack/test/accessibility/apps/group1/search_profiler.ts index 30043f8f4157f..522c5e4cf730e 100644 --- a/x-pack/test/accessibility/apps/search_profiler.ts +++ b/x-pack/test/accessibility/apps/group1/search_profiler.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'security']); diff --git a/x-pack/test/accessibility/apps/spaces.ts b/x-pack/test/accessibility/apps/group1/spaces.ts similarity index 98% rename from x-pack/test/accessibility/apps/spaces.ts rename to x-pack/test/accessibility/apps/group1/spaces.ts index 622b1b3cefd64..33616c3576b1d 100644 --- a/x-pack/test/accessibility/apps/spaces.ts +++ b/x-pack/test/accessibility/apps/group1/spaces.ts @@ -7,7 +7,7 @@ // a11y tests for spaces, space selection and space creation and feature controls -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'spaceSelector', 'home', 'header', 'security']); diff --git a/x-pack/test/accessibility/apps/uptime.ts b/x-pack/test/accessibility/apps/group1/uptime.ts similarity index 91% rename from x-pack/test/accessibility/apps/uptime.ts rename to x-pack/test/accessibility/apps/group1/uptime.ts index 49243c37fe730..3818ddb1061a2 100644 --- a/x-pack/test/accessibility/apps/uptime.ts +++ b/x-pack/test/accessibility/apps/group1/uptime.ts @@ -6,8 +6,8 @@ */ import moment from 'moment'; -import { FtrProviderContext } from '../ftr_provider_context'; -import { makeChecks } from '../../api_integration/apis/uptime/rest/helper/make_checks'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { makeChecks } from '../../../api_integration/apis/uptime/rest/helper/make_checks'; const A11Y_TEST_MONITOR_ID = 'a11yTestMonitor'; @@ -19,7 +19,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const es = getService('es'); const toasts = getService('toasts'); - describe('uptime Accessibility', () => { + // github.com/elastic/kibana/issues/153601 + describe.skip('uptime Accessibility', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/uptime/blank'); await makeChecks(es, A11Y_TEST_MONITOR_ID, 150, 1, 1000, { diff --git a/x-pack/test/accessibility/apps/users.ts b/x-pack/test/accessibility/apps/group1/users.ts similarity index 98% rename from x-pack/test/accessibility/apps/users.ts rename to x-pack/test/accessibility/apps/group1/users.ts index 6057b4d45bb09..e26e6a6f6a54f 100644 --- a/x-pack/test/accessibility/apps/users.ts +++ b/x-pack/test/accessibility/apps/group1/users.ts @@ -7,7 +7,7 @@ // a11y tests for spaces, space selection and spacce creation and feature controls -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['security', 'settings']); diff --git a/x-pack/test/accessibility/apps/group2/config.ts b/x-pack/test/accessibility/apps/group2/config.ts new file mode 100644 index 0000000000000..27cf620bc05c8 --- /dev/null +++ b/x-pack/test/accessibility/apps/group2/config.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; +import { services } from '../../services'; +import { pageObjects } from '../../page_objects'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile( + require.resolve('../../../functional/config.base.js') + ); + + return { + ...functionalConfig.getAll(), + + testFiles: [require.resolve('.')], + + pageObjects, + services, + + junit: { + reportName: 'X-Pack Accessibility Tests - Group 2', + }, + }; +} diff --git a/x-pack/test/accessibility/apps/group2/index.ts b/x-pack/test/accessibility/apps/group2/index.ts new file mode 100644 index 0000000000000..2c6bf4e58a08b --- /dev/null +++ b/x-pack/test/accessibility/apps/group2/index.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 { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('X-Pack Accessibility Tests - Group 2', function () { + loadTestFile(require.resolve('./ml')); + loadTestFile(require.resolve('./ml_anomaly_detection')); + loadTestFile(require.resolve('./transform')); + loadTestFile(require.resolve('./lens')); + }); +}; diff --git a/x-pack/test/accessibility/apps/lens.ts b/x-pack/test/accessibility/apps/group2/lens.ts similarity index 98% rename from x-pack/test/accessibility/apps/lens.ts rename to x-pack/test/accessibility/apps/group2/lens.ts index 1153d61d1fc68..860138fc77701 100644 --- a/x-pack/test/accessibility/apps/lens.ts +++ b/x-pack/test/accessibility/apps/group2/lens.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'visualize', 'timePicker', 'home', 'lens']); @@ -25,11 +25,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); after(async () => { - await PageObjects.common.navigateToApp('visualize'); - await listingTable.searchForItemWithName(lensChartName); - await listingTable.checkListingSelectAllCheckbox(); - await listingTable.clickDeleteSelected(); - await PageObjects.common.clickConfirmOnModal(); await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); await kibanaServer.importExport.unload( 'x-pack/test/functional/fixtures/kbn_archiver/lens/lens_basic.json' @@ -173,6 +168,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('saves lens chart', async () => { await PageObjects.lens.save(lensChartName); await a11y.testAppSnapshot(); + // delete newly created Lens + await PageObjects.common.navigateToApp('visualize'); + await listingTable.searchForItemWithName(lensChartName); + await listingTable.checkListingSelectAllCheckbox(); + await listingTable.clickDeleteSelected(); + await PageObjects.common.clickConfirmOnModal(); }); }); } diff --git a/x-pack/test/accessibility/apps/ml.ts b/x-pack/test/accessibility/apps/group2/ml.ts similarity index 98% rename from x-pack/test/accessibility/apps/ml.ts rename to x-pack/test/accessibility/apps/group2/ml.ts index 2a6021b7be78e..af9664b5e258a 100644 --- a/x-pack/test/accessibility/apps/ml.ts +++ b/x-pack/test/accessibility/apps/group2/ml.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const a11y = getService('a11y'); @@ -68,7 +68,7 @@ export default function ({ getService }: FtrProviderContext) { const dfaClassificationJobTrainingPercent = 30; const uploadFilePath = require.resolve( - '../../functional/apps/ml/data_visualizer/files_to_import/artificial_server_log' + '../../../functional/apps/ml/data_visualizer/files_to_import/artificial_server_log' ); before(async () => { diff --git a/x-pack/test/accessibility/apps/ml_anomaly_detection.ts b/x-pack/test/accessibility/apps/group2/ml_anomaly_detection.ts similarity index 99% rename from x-pack/test/accessibility/apps/ml_anomaly_detection.ts rename to x-pack/test/accessibility/apps/group2/ml_anomaly_detection.ts index 39860093871e5..33c7db9eb0b20 100644 --- a/x-pack/test/accessibility/apps/ml_anomaly_detection.ts +++ b/x-pack/test/accessibility/apps/group2/ml_anomaly_detection.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; interface Detector { identifier: string; diff --git a/x-pack/test/accessibility/apps/transform.ts b/x-pack/test/accessibility/apps/group2/transform.ts similarity index 99% rename from x-pack/test/accessibility/apps/transform.ts rename to x-pack/test/accessibility/apps/group2/transform.ts index b1228b6c7155a..2e28a7e75fb2a 100644 --- a/x-pack/test/accessibility/apps/transform.ts +++ b/x-pack/test/accessibility/apps/group2/transform.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const a11y = getService('a11y'); diff --git a/x-pack/test/accessibility/apps/canvas.ts b/x-pack/test/accessibility/apps/group3/canvas.ts similarity index 96% rename from x-pack/test/accessibility/apps/canvas.ts rename to x-pack/test/accessibility/apps/group3/canvas.ts index f3dfa9305fb95..fb6cc672e2ffa 100644 --- a/x-pack/test/accessibility/apps/canvas.ts +++ b/x-pack/test/accessibility/apps/group3/canvas.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); diff --git a/x-pack/test/accessibility/apps/group3/config.ts b/x-pack/test/accessibility/apps/group3/config.ts new file mode 100644 index 0000000000000..94f6c862b5396 --- /dev/null +++ b/x-pack/test/accessibility/apps/group3/config.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; +import { services } from '../../services'; +import { pageObjects } from '../../page_objects'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile( + require.resolve('../../../functional/config.base.js') + ); + + return { + ...functionalConfig.getAll(), + + testFiles: [require.resolve('.')], + + pageObjects, + services, + + junit: { + reportName: 'X-Pack Accessibility Tests - Group 3', + }, + }; +} diff --git a/x-pack/test/accessibility/apps/cross_cluster_replication.ts b/x-pack/test/accessibility/apps/group3/cross_cluster_replication.ts similarity index 95% rename from x-pack/test/accessibility/apps/cross_cluster_replication.ts rename to x-pack/test/accessibility/apps/group3/cross_cluster_replication.ts index bc81770de9f4b..db5d70ac26d04 100644 --- a/x-pack/test/accessibility/apps/cross_cluster_replication.ts +++ b/x-pack/test/accessibility/apps/group3/cross_cluster_replication.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects([ @@ -20,7 +20,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const es = getService('es'); const retry = getService('retry'); - describe('cross cluster replication - a11y tests', async () => { + // github.com/elastic/kibana/issues/153599 + describe.skip('cross cluster replication - a11y tests', async () => { before(async () => { await PageObjects.common.navigateToApp('crossClusterReplication'); }); diff --git a/x-pack/test/accessibility/apps/enterprise_search.ts b/x-pack/test/accessibility/apps/group3/enterprise_search.ts similarity index 98% rename from x-pack/test/accessibility/apps/enterprise_search.ts rename to x-pack/test/accessibility/apps/group3/enterprise_search.ts index b610130d99e0e..a7b7724e43181 100644 --- a/x-pack/test/accessibility/apps/enterprise_search.ts +++ b/x-pack/test/accessibility/apps/group3/enterprise_search.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); diff --git a/x-pack/test/accessibility/apps/graph.ts b/x-pack/test/accessibility/apps/group3/graph.ts similarity index 98% rename from x-pack/test/accessibility/apps/graph.ts rename to x-pack/test/accessibility/apps/group3/graph.ts index 03ca3b2afbfe4..839ea50ce2779 100644 --- a/x-pack/test/accessibility/apps/graph.ts +++ b/x-pack/test/accessibility/apps/group3/graph.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); diff --git a/x-pack/test/accessibility/apps/group3/grok_debugger.ts b/x-pack/test/accessibility/apps/group3/grok_debugger.ts new file mode 100644 index 0000000000000..da630c6bed7b3 --- /dev/null +++ b/x-pack/test/accessibility/apps/group3/grok_debugger.ts @@ -0,0 +1,37 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'security']); + const a11y = getService('a11y'); + const grokDebugger = getService('grokDebugger'); + + // Fixes:https://github.com/elastic/kibana/issues/62102 + describe('Dev tools grok debugger', () => { + before(async () => { + await PageObjects.common.navigateToApp('grokDebugger'); + await grokDebugger.assertExists(); + }); + + it('Dev tools grok debugger set input', async () => { + await grokDebugger.setEventInput('SegerCommaBob'); + await a11y.testAppSnapshot(); + }); + + it('Dev tools grok debugger set pattern', async () => { + await grokDebugger.setPatternInput('%{USERNAME:u}'); + await a11y.testAppSnapshot(); + }); + + it('Dev tools grok debugger simulate', async () => { + await grokDebugger.clickSimulate(); + await a11y.testAppSnapshot(); + }); + }); +} diff --git a/x-pack/test/accessibility/apps/group3/index.ts b/x-pack/test/accessibility/apps/group3/index.ts new file mode 100644 index 0000000000000..d295c2a17a4f0 --- /dev/null +++ b/x-pack/test/accessibility/apps/group3/index.ts @@ -0,0 +1,35 @@ +/* + * 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 { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('X-Pack Accessibility Tests - Group 3', function () { + loadTestFile(require.resolve('./upgrade_assistant')); + loadTestFile(require.resolve('./canvas')); + loadTestFile(require.resolve('./maps')); + loadTestFile(require.resolve('./graph')); + loadTestFile(require.resolve('./security_solution')); + loadTestFile(require.resolve('./ml_embeddables_in_dashboard')); + loadTestFile(require.resolve('./rules_connectors')); + // Please make sure that the remote clusters, snapshot and restore and + // CCR tests stay in that order. Their execution fails if rearranged. + loadTestFile(require.resolve('./remote_clusters')); + loadTestFile(require.resolve('./snapshot_and_restore')); + loadTestFile(require.resolve('./cross_cluster_replication')); + loadTestFile(require.resolve('./reporting')); + loadTestFile(require.resolve('./enterprise_search')); + + // loadTestFile(require.resolve('./license_management')); + // loadTestFile(require.resolve('./tags')); + // loadTestFile(require.resolve('./search_sessions')); + // loadTestFile(require.resolve('./stack_monitoring')); + // loadTestFile(require.resolve('./watcher')); + // loadTestFile(require.resolve('./rollup_jobs')); + // loadTestFile(require.resolve('./observability')); + }); +}; diff --git a/x-pack/test/accessibility/apps/license_management.ts b/x-pack/test/accessibility/apps/group3/license_management.ts similarity index 95% rename from x-pack/test/accessibility/apps/license_management.ts rename to x-pack/test/accessibility/apps/group3/license_management.ts index 7693ebb197ff1..a71ac90f54ce8 100644 --- a/x-pack/test/accessibility/apps/license_management.ts +++ b/x-pack/test/accessibility/apps/group3/license_management.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['licenseManagement', 'common']); diff --git a/x-pack/test/accessibility/apps/maps.ts b/x-pack/test/accessibility/apps/group3/maps.ts similarity index 98% rename from x-pack/test/accessibility/apps/maps.ts rename to x-pack/test/accessibility/apps/group3/maps.ts index af74466fb8f24..2f69696b5824f 100644 --- a/x-pack/test/accessibility/apps/maps.ts +++ b/x-pack/test/accessibility/apps/group3/maps.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); diff --git a/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts b/x-pack/test/accessibility/apps/group3/ml_embeddables_in_dashboard.ts similarity index 98% rename from x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts rename to x-pack/test/accessibility/apps/group3/ml_embeddables_in_dashboard.ts index 71933514de001..c3278b39096c7 100644 --- a/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts +++ b/x-pack/test/accessibility/apps/group3/ml_embeddables_in_dashboard.ts @@ -6,7 +6,7 @@ */ import { Datafeed, Job } from '@kbn/ml-plugin/common/types/anomaly_detection_jobs'; -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; // @ts-expect-error not full interface const JOB_CONFIG: Job = { diff --git a/x-pack/test/accessibility/apps/observability.ts b/x-pack/test/accessibility/apps/group3/observability.ts similarity index 95% rename from x-pack/test/accessibility/apps/observability.ts rename to x-pack/test/accessibility/apps/group3/observability.ts index ead89c913d1e1..1d24c1c17be24 100644 --- a/x-pack/test/accessibility/apps/observability.ts +++ b/x-pack/test/accessibility/apps/group3/observability.ts @@ -6,7 +6,7 @@ */ // a11y tests for spaces, space selection and space creation and feature controls -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'infraHome']); diff --git a/x-pack/test/accessibility/apps/remote_clusters.ts b/x-pack/test/accessibility/apps/group3/remote_clusters.ts similarity index 99% rename from x-pack/test/accessibility/apps/remote_clusters.ts rename to x-pack/test/accessibility/apps/group3/remote_clusters.ts index deb0e4a090b8c..4b509c88f0525 100644 --- a/x-pack/test/accessibility/apps/remote_clusters.ts +++ b/x-pack/test/accessibility/apps/group3/remote_clusters.ts @@ -6,7 +6,7 @@ */ import { ClusterPayloadEs } from '@kbn/remote-clusters-plugin/common/lib'; -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; const emptyPrompt = 'remoteClusterListEmptyPrompt'; const createButton = 'remoteClusterEmptyPromptCreateButton'; diff --git a/x-pack/test/accessibility/apps/reporting.ts b/x-pack/test/accessibility/apps/group3/reporting.ts similarity index 97% rename from x-pack/test/accessibility/apps/reporting.ts rename to x-pack/test/accessibility/apps/group3/reporting.ts index f1ac0770c9587..45959e42b383a 100644 --- a/x-pack/test/accessibility/apps/reporting.ts +++ b/x-pack/test/accessibility/apps/group3/reporting.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const { common } = getPageObjects(['common']); diff --git a/x-pack/test/accessibility/apps/rollup_jobs.ts b/x-pack/test/accessibility/apps/group3/rollup_jobs.ts similarity index 98% rename from x-pack/test/accessibility/apps/rollup_jobs.ts rename to x-pack/test/accessibility/apps/group3/rollup_jobs.ts index 2acf48d5f049f..5581a11955e18 100644 --- a/x-pack/test/accessibility/apps/rollup_jobs.ts +++ b/x-pack/test/accessibility/apps/group3/rollup_jobs.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'settings', 'header', 'rollup']); diff --git a/x-pack/test/accessibility/apps/rules_connectors.ts b/x-pack/test/accessibility/apps/group3/rules_connectors.ts similarity index 98% rename from x-pack/test/accessibility/apps/rules_connectors.ts rename to x-pack/test/accessibility/apps/group3/rules_connectors.ts index bc17793717282..bf46735a84fde 100644 --- a/x-pack/test/accessibility/apps/rules_connectors.ts +++ b/x-pack/test/accessibility/apps/group3/rules_connectors.ts @@ -7,7 +7,7 @@ // a11y tests for rules, logs and connectors page -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['settings', 'common']); diff --git a/x-pack/test/accessibility/apps/search_sessions.ts b/x-pack/test/accessibility/apps/group3/search_sessions.ts similarity index 98% rename from x-pack/test/accessibility/apps/search_sessions.ts rename to x-pack/test/accessibility/apps/group3/search_sessions.ts index 5a4ca433e3d21..c400d17263221 100644 --- a/x-pack/test/accessibility/apps/search_sessions.ts +++ b/x-pack/test/accessibility/apps/group3/search_sessions.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['searchSessionsManagement']); diff --git a/x-pack/test/accessibility/apps/security_solution.ts b/x-pack/test/accessibility/apps/group3/security_solution.ts similarity index 98% rename from x-pack/test/accessibility/apps/security_solution.ts rename to x-pack/test/accessibility/apps/group3/security_solution.ts index ba7d22fd2d39d..7257f88be8a74 100644 --- a/x-pack/test/accessibility/apps/security_solution.ts +++ b/x-pack/test/accessibility/apps/group3/security_solution.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); diff --git a/x-pack/test/accessibility/apps/snapshot_and_restore.ts b/x-pack/test/accessibility/apps/group3/snapshot_and_restore.ts similarity index 98% rename from x-pack/test/accessibility/apps/snapshot_and_restore.ts rename to x-pack/test/accessibility/apps/group3/snapshot_and_restore.ts index c5f0f52c9c9fe..4c3e5121f9830 100644 --- a/x-pack/test/accessibility/apps/snapshot_and_restore.ts +++ b/x-pack/test/accessibility/apps/group3/snapshot_and_restore.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'settings', 'header', 'snapshotRestore']); diff --git a/x-pack/test/accessibility/apps/stack_monitoring.ts b/x-pack/test/accessibility/apps/group3/stack_monitoring.ts similarity index 97% rename from x-pack/test/accessibility/apps/stack_monitoring.ts rename to x-pack/test/accessibility/apps/group3/stack_monitoring.ts index 87bd4d64d1fb8..64eaa652e702e 100644 --- a/x-pack/test/accessibility/apps/stack_monitoring.ts +++ b/x-pack/test/accessibility/apps/group3/stack_monitoring.ts @@ -7,7 +7,7 @@ // a11y tests for stack monitoring import expect from '@kbn/expect'; -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'spaceSelector', 'home', 'header', 'security']); diff --git a/x-pack/test/accessibility/apps/tags.ts b/x-pack/test/accessibility/apps/group3/tags.ts similarity index 98% rename from x-pack/test/accessibility/apps/tags.ts rename to x-pack/test/accessibility/apps/group3/tags.ts index 0c0f836cfc894..f885e8ba77c07 100644 --- a/x-pack/test/accessibility/apps/tags.ts +++ b/x-pack/test/accessibility/apps/group3/tags.ts @@ -7,7 +7,7 @@ // a11y tests for spaces, space selection and space creation and feature controls -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'settings', 'header', 'home', 'tagManagement']); diff --git a/x-pack/test/accessibility/apps/upgrade_assistant.ts b/x-pack/test/accessibility/apps/group3/upgrade_assistant.ts similarity index 99% rename from x-pack/test/accessibility/apps/upgrade_assistant.ts rename to x-pack/test/accessibility/apps/group3/upgrade_assistant.ts index 47582893e771f..02f823ef6a674 100644 --- a/x-pack/test/accessibility/apps/upgrade_assistant.ts +++ b/x-pack/test/accessibility/apps/group3/upgrade_assistant.ts @@ -11,7 +11,7 @@ */ import type { IndicesCreateRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; const translogSettingsIndexDeprecation: IndicesCreateRequest = { index: 'deprecated_settings', diff --git a/x-pack/test/accessibility/apps/watcher.ts b/x-pack/test/accessibility/apps/group3/watcher.ts similarity index 95% rename from x-pack/test/accessibility/apps/watcher.ts rename to x-pack/test/accessibility/apps/group3/watcher.ts index 85a11db0122ab..72b4e87e50660 100644 --- a/x-pack/test/accessibility/apps/watcher.ts +++ b/x-pack/test/accessibility/apps/group3/watcher.ts @@ -7,7 +7,7 @@ // a11y tests for spaces, space selection and space creation and feature controls -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'home', 'header', 'watcher', 'security']); diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts deleted file mode 100644 index 1475b3aeff8af..0000000000000 --- a/x-pack/test/accessibility/config.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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 { FtrConfigProviderContext } from '@kbn/test'; -import { services } from './services'; -import { pageObjects } from './page_objects'; - -export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); - - return { - ...functionalConfig.getAll(), - - testFiles: [ - require.resolve('./apps/login_page'), - require.resolve('./apps/kibana_overview'), - require.resolve('./apps/home'), - require.resolve('./apps/management'), - require.resolve('./apps/grok_debugger'), - require.resolve('./apps/search_profiler'), - require.resolve('./apps/painless_lab'), - // https://github.com/elastic/kibana/issues/153601 - // require.resolve('./apps/uptime'), - require.resolve('./apps/spaces'), - require.resolve('./apps/advanced_settings'), - require.resolve('./apps/dashboard_panel_options'), - require.resolve('./apps/dashboard_controls'), - require.resolve('./apps/users'), - require.resolve('./apps/roles'), - require.resolve('./apps/ingest_node_pipelines'), - require.resolve('./apps/index_lifecycle_management'), - require.resolve('./apps/ml'), - require.resolve('./apps/ml_anomaly_detection'), - require.resolve('./apps/transform'), - require.resolve('./apps/lens'), - require.resolve('./apps/upgrade_assistant'), - require.resolve('./apps/canvas'), - require.resolve('./apps/maps'), - require.resolve('./apps/graph'), - require.resolve('./apps/security_solution'), - require.resolve('./apps/ml_embeddables_in_dashboard'), - require.resolve('./apps/rules_connectors'), - // Please make sure that the remote clusters, snapshot and restore and - // CCR tests stay in that order. Their execution fails if rearranged. - require.resolve('./apps/remote_clusters'), - require.resolve('./apps/snapshot_and_restore'), - require.resolve('./apps/cross_cluster_replication'), - require.resolve('./apps/reporting'), - require.resolve('./apps/enterprise_search'), - // require.resolve('./apps/license_management'), - // require.resolve('./apps/tags'), - // require.resolve('./apps/search_sessions'), - // require.resolve('./apps/stack_monitoring'), - // require.resolve('./apps/watcher'), - // require.resolve('./apps/rollup_jobs'), - // require.resolve('./apps/observability'), - ], - - pageObjects, - services, - - junit: { - reportName: 'X-Pack Accessibility Tests', - }, - }; -} From 242cb6f1d5cf97766fe8a5697488877e2390befe Mon Sep 17 00:00:00 2001 From: Gloria Hornero Date: Tue, 28 Nov 2023 14:35:17 +0100 Subject: [PATCH 11/14] [Security Solution] Specific Cypress executions for `Rule Management` team (#171868) Co-authored-by: Georgii Gorbachev --- .../verify_es_serverless_image.yml | 26 ++++++++++ .buildkite/pipelines/on_merge.yml | 48 +++++++++++++++++++ .buildkite/pipelines/pull_request/base.yml | 48 +++++++++++++++++++ .../security_solution_cypress.yml | 24 ++++++++++ .../security_serverless_rule_management.sh | 16 +++++++ ...rverless_rule_management_prebuilt_rules.sh | 16 +++++++ .../security_solution_rule_management.sh | 16 +++++++ ...solution_rule_management_prebuilt_rules.sh | 16 +++++++ .github/CODEOWNERS | 2 - .../cypress/README.md | 23 ++++++--- .../install_update_authorization.cy.ts | 12 ++--- .../install_update_error_handling.cy.ts | 14 +++--- .../prebuilt_rules/install_via_fleet.cy.ts | 14 +++--- .../prebuilt_rules/install_workflow.cy.ts | 20 ++++---- .../prebuilt_rules/management.cy.ts | 21 ++++---- .../prebuilt_rules/notifications.cy.ts | 19 ++++---- .../prebuilt_rules_preview.cy.ts | 24 +++++----- .../prebuilt_rules/update_workflow.ts | 18 +++---- .../rule_details/common_flows.cy.ts | 26 +++++----- .../rule_details/esql_rule.cy.ts | 16 +++---- .../security_solution_cypress/package.json | 14 ++++-- 21 files changed, 332 insertions(+), 101 deletions(-) create mode 100644 .buildkite/scripts/steps/functional/security_serverless_rule_management.sh create mode 100644 .buildkite/scripts/steps/functional/security_serverless_rule_management_prebuilt_rules.sh create mode 100644 .buildkite/scripts/steps/functional/security_solution_rule_management.sh create mode 100644 .buildkite/scripts/steps/functional/security_solution_rule_management_prebuilt_rules.sh rename x-pack/test/security_solution_cypress/cypress/e2e/detection_response/{ => rule_management}/prebuilt_rules/install_update_authorization.cy.ts (94%) rename x-pack/test/security_solution_cypress/cypress/e2e/detection_response/{ => rule_management}/prebuilt_rules/install_update_error_handling.cy.ts (94%) rename x-pack/test/security_solution_cypress/cypress/e2e/detection_response/{ => rule_management}/prebuilt_rules/install_via_fleet.cy.ts (90%) rename x-pack/test/security_solution_cypress/cypress/e2e/detection_response/{ => rule_management}/prebuilt_rules/install_workflow.cy.ts (85%) rename x-pack/test/security_solution_cypress/cypress/e2e/detection_response/{ => rule_management}/prebuilt_rules/management.cy.ts (91%) rename x-pack/test/security_solution_cypress/cypress/e2e/detection_response/{ => rule_management}/prebuilt_rules/notifications.cy.ts (92%) rename x-pack/test/security_solution_cypress/cypress/e2e/detection_response/{ => rule_management}/prebuilt_rules/prebuilt_rules_preview.cy.ts (97%) rename x-pack/test/security_solution_cypress/cypress/e2e/detection_response/{ => rule_management}/prebuilt_rules/update_workflow.ts (88%) rename x-pack/test/security_solution_cypress/cypress/e2e/detection_response/{ => rule_management}/rule_details/common_flows.cy.ts (88%) rename x-pack/test/security_solution_cypress/cypress/e2e/detection_response/{ => rule_management}/rule_details/esql_rule.cy.ts (69%) diff --git a/.buildkite/pipelines/es_serverless/verify_es_serverless_image.yml b/.buildkite/pipelines/es_serverless/verify_es_serverless_image.yml index 8e64513b14900..8d1b778b67983 100644 --- a/.buildkite/pipelines/es_serverless/verify_es_serverless_image.yml +++ b/.buildkite/pipelines/es_serverless/verify_es_serverless_image.yml @@ -95,6 +95,32 @@ steps: - exit_status: '*' limit: 1 + - command: .buildkite/scripts/steps/functional/security_serverless_rule_management.sh + label: 'Serverless Rule Management - Security Solution Cypress Tests' + if: "build.env('SKIP_CYPRESS') != '1' && build.env('SKIP_CYPRESS') != 'true'" + agents: + queue: n2-4-spot + depends_on: build + timeout_in_minutes: 60 + parallelism: 8 + retry: + automatic: + - exit_status: '*' + limit: 1 + + - command: .buildkite/scripts/steps/functional/security_serverless_rule_management_prebuilt_rules.sh + label: 'Serverless Rule Management - Prebuilt Rules - Security Solution Cypress Tests' + if: "build.env('SKIP_CYPRESS') != '1' && build.env('SKIP_CYPRESS') != 'true'" + agents: + queue: n2-4-spot + depends_on: build + timeout_in_minutes: 60 + parallelism: 6 + retry: + automatic: + - exit_status: '*' + limit: 1 + - command: .buildkite/scripts/steps/functional/defend_workflows_serverless.sh label: 'Defend Workflows Cypress Tests on Serverless' if: "build.env('SKIP_CYPRESS') != '1' && build.env('SKIP_CYPRESS') != 'true'" diff --git a/.buildkite/pipelines/on_merge.yml b/.buildkite/pipelines/on_merge.yml index 8b00db428a713..8256eb2395633 100644 --- a/.buildkite/pipelines/on_merge.yml +++ b/.buildkite/pipelines/on_merge.yml @@ -115,6 +115,54 @@ steps: - exit_status: '*' limit: 1 + - command: .buildkite/scripts/steps/functional/security_serverless_rule_management.sh + label: 'Serverless Rule Management - Security Solution Cypress Tests' + agents: + queue: n2-4-spot + depends_on: build + timeout_in_minutes: 60 + parallelism: 8 + retry: + automatic: + - exit_status: '*' + limit: 1 + + - command: .buildkite/scripts/steps/functional/security_serverless_rule_management_prebuilt_rules.sh + label: 'Serverless Rule Management - Prebuilt Rules - Security Solution Cypress Tests' + agents: + queue: n2-4-spot + depends_on: build + timeout_in_minutes: 60 + parallelism: 6 + retry: + automatic: + - exit_status: '*' + limit: 1 + + - command: .buildkite/scripts/steps/functional/security_solution_rule_management.sh + label: 'Rule Management - Security Solution Cypress Tests' + agents: + queue: n2-4-spot + depends_on: build + timeout_in_minutes: 60 + parallelism: 8 + retry: + automatic: + - exit_status: '*' + limit: 1 + + - command: .buildkite/scripts/steps/functional/security_solution_rule_management_prebuilt_rules.sh + label: 'Rule Management - Prebuilt Rules - Security Solution Cypress Tests' + agents: + queue: n2-4-spot + depends_on: build + timeout_in_minutes: 60 + parallelism: 6 + retry: + automatic: + - exit_status: '*' + limit: 1 + - command: .buildkite/scripts/steps/functional/security_solution.sh label: 'Security Solution Cypress Tests' agents: diff --git a/.buildkite/pipelines/pull_request/base.yml b/.buildkite/pipelines/pull_request/base.yml index 49215bbd00f11..8238afbee4fd2 100644 --- a/.buildkite/pipelines/pull_request/base.yml +++ b/.buildkite/pipelines/pull_request/base.yml @@ -93,6 +93,30 @@ steps: - exit_status: '*' limit: 1 + - command: .buildkite/scripts/steps/functional/security_serverless_rule_management.sh + label: 'Serverless Rule Management - Security Solution Cypress Tests' + agents: + queue: n2-4-spot + depends_on: build + timeout_in_minutes: 60 + parallelism: 8 + retry: + automatic: + - exit_status: '*' + limit: 1 + + - command: .buildkite/scripts/steps/functional/security_serverless_rule_management_prebuilt_rules.sh + label: 'Serverless Rule Management - Prebuilt Rules - Security Solution Cypress Tests' + agents: + queue: n2-4-spot + depends_on: build + timeout_in_minutes: 60 + parallelism: 6 + retry: + automatic: + - exit_status: '*' + limit: 1 + - command: .buildkite/scripts/steps/functional/security_solution.sh label: 'Security Solution Cypress Tests' agents: @@ -117,6 +141,30 @@ steps: - exit_status: '*' limit: 1 + - command: .buildkite/scripts/steps/functional/security_solution_rule_management.sh + label: 'Rule Management - Security Solution Cypress Tests' + agents: + queue: n2-4-spot + depends_on: build + timeout_in_minutes: 60 + parallelism: 8 + retry: + automatic: + - exit_status: '*' + limit: 1 + + - command: .buildkite/scripts/steps/functional/security_solution_rule_management_prebuilt_rules.sh + label: 'Rule Management - Prebuilt Rules - Security Solution Cypress Tests' + agents: + queue: n2-4-spot + depends_on: build + timeout_in_minutes: 60 + parallelism: 6 + retry: + automatic: + - exit_status: '*' + limit: 1 + - command: .buildkite/scripts/steps/functional/security_solution_investigations.sh label: 'Investigations - Security Solution Cypress Tests' agents: diff --git a/.buildkite/pipelines/security_solution/security_solution_cypress.yml b/.buildkite/pipelines/security_solution/security_solution_cypress.yml index 247505ef1c85a..77e7fea574352 100644 --- a/.buildkite/pipelines/security_solution/security_solution_cypress.yml +++ b/.buildkite/pipelines/security_solution/security_solution_cypress.yml @@ -30,6 +30,30 @@ steps: # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. timeout_in_minutes: 300 parallelism: 8 + retry: + automatic: + - exit_status: '*' + limit: 1 + + - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:rule_management + label: 'Serverless MKI QA Rule Management - Security Solution Cypress Tests' + agents: + queue: n2-4-spot + # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. + timeout_in_minutes: 300 + parallelism: 8 + retry: + automatic: + - exit_status: '*' + limit: 1 + + - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:rule_management:prebuilt_rules + label: 'Serverless MKI QA Rule Management - Prebuilt Rules - Security Solution Cypress Tests' + agents: + queue: n2-4-spot + # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. + timeout_in_minutes: 300 + parallelism: 6 retry: automatic: - exit_status: '*' diff --git a/.buildkite/scripts/steps/functional/security_serverless_rule_management.sh b/.buildkite/scripts/steps/functional/security_serverless_rule_management.sh new file mode 100644 index 0000000000000..5d360e0db4f29 --- /dev/null +++ b/.buildkite/scripts/steps/functional/security_serverless_rule_management.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source .buildkite/scripts/steps/functional/common.sh +source .buildkite/scripts/steps/functional/common_cypress.sh + +export JOB=kibana-security-solution-chrome +export KIBANA_INSTALL_DIR=${KIBANA_BUILD_LOCATION} + +echo "--- Rule Management Cypress Tests on Serverless" + +cd x-pack/test/security_solution_cypress + +set +e +yarn cypress:rule_management:run:serverless; status=$?; yarn junit:merge || :; exit $status diff --git a/.buildkite/scripts/steps/functional/security_serverless_rule_management_prebuilt_rules.sh b/.buildkite/scripts/steps/functional/security_serverless_rule_management_prebuilt_rules.sh new file mode 100644 index 0000000000000..bc7dc3269d8cb --- /dev/null +++ b/.buildkite/scripts/steps/functional/security_serverless_rule_management_prebuilt_rules.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source .buildkite/scripts/steps/functional/common.sh +source .buildkite/scripts/steps/functional/common_cypress.sh + +export JOB=kibana-security-solution-chrome +export KIBANA_INSTALL_DIR=${KIBANA_BUILD_LOCATION} + +echo "--- Rule Management - Prebuilt Rules - Cypress Tests on Serverless" + +cd x-pack/test/security_solution_cypress + +set +e +yarn cypress:rule_management:prebuilt_rules:run:serverless; status=$?; yarn junit:merge || :; exit $status diff --git a/.buildkite/scripts/steps/functional/security_solution_rule_management.sh b/.buildkite/scripts/steps/functional/security_solution_rule_management.sh new file mode 100644 index 0000000000000..847cb42896cf1 --- /dev/null +++ b/.buildkite/scripts/steps/functional/security_solution_rule_management.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source .buildkite/scripts/steps/functional/common.sh +source .buildkite/scripts/steps/functional/common_cypress.sh + +export JOB=kibana-security-solution-chrome +export KIBANA_INSTALL_DIR=${KIBANA_BUILD_LOCATION} + +echo "--- Rule Management - Security Solution Cypress Tests" + +cd x-pack/test/security_solution_cypress + +set +e +yarn cypress:rule_management:run:ess; status=$?; yarn junit:merge || :; exit $status diff --git a/.buildkite/scripts/steps/functional/security_solution_rule_management_prebuilt_rules.sh b/.buildkite/scripts/steps/functional/security_solution_rule_management_prebuilt_rules.sh new file mode 100644 index 0000000000000..d8b19ad3363b5 --- /dev/null +++ b/.buildkite/scripts/steps/functional/security_solution_rule_management_prebuilt_rules.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source .buildkite/scripts/steps/functional/common.sh +source .buildkite/scripts/steps/functional/common_cypress.sh + +export JOB=kibana-security-solution-chrome +export KIBANA_INSTALL_DIR=${KIBANA_BUILD_LOCATION} + +echo "--- Rule Management - Prebuilt Rules - Security Solution Cypress Tests" + +cd x-pack/test/security_solution_cypress + +set +e +yarn cypress:rule_management:prebuilt_rules:run:ess; status=$?; yarn junit:merge || :; exit $status diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 89e152a2fe40f..7d075295240c9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1322,9 +1322,7 @@ x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout @elastic/ /x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring @elastic/security-detection-rule-management /x-pack/plugins/security_solution/common/detection_engine/rule_management @elastic/security-detection-rule-management -/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules @elastic/security-detection-rule-management /x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management @elastic/security-detection-rule-management -/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_details @elastic/security-detection-rule-management /x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules @elastic/security-detection-rule-management /x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/rule_management @elastic/security-detection-rule-management /x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/prebuilt_rules @elastic/security-detection-rule-management diff --git a/x-pack/test/security_solution_cypress/cypress/README.md b/x-pack/test/security_solution_cypress/cypress/README.md index 8940d6c86e73e..88786aed7ff56 100644 --- a/x-pack/test/security_solution_cypress/cypress/README.md +++ b/x-pack/test/security_solution_cypress/cypress/README.md @@ -62,19 +62,25 @@ Run the tests with the following yarn scripts from `x-pack/test/security_solutio | cypress | Runs the default Cypress command | | cypress:open:ess | Opens the Cypress UI with all tests in the `e2e` directory. This also runs a local kibana and ES instance. The kibana instance will reload when you make code changes. This is the recommended way to debug and develop tests. | | cypress:open:serverless | Opens the Cypress UI with all tests in the `e2e` directory. This also runs a mocked serverless environment. The kibana instance will reload when you make code changes. This is the recommended way to debug and develop tests. | -| cypress:run:ess | Runs all tests tagged as ESS placed in the `e2e` directory excluding `investigations` and `explore` directories in headless mode | +| cypress:run:ess | Runs all tests tagged as ESS placed in the `e2e` directory excluding `investigations`,`explore` and `detection_response/rule_management` directories in headless mode | | cypress:run:cases:ess | Runs all tests under `explore/cases` in the `e2e` directory related to the Cases area team in headless mode | | cypress:ess | Runs all ESS tests with the specified configuration in headless mode and produces a report using `cypress-multi-reporters` | +| cypress:rule_management:run:ess | Runs all tests tagged as ESS in the `e2e/detection_response/rule_management` excluding `e2e/detection_response/rule_management/prebuilt_rules` directory in headless mode | +| cypress:rule_management:prebuilt_rules:run:ess | Runs all tests tagged as ESS in the `e2e/detection_response/rule_management/prebuilt_rules` directory in headless mode | | cypress:run:respops:ess | Runs all tests related to the Response Ops area team, specifically tests in `detection_alerts`, `detection_rules`, and `exceptions` directories in headless mode | -| cypress:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e` directory excluding `investigations` and `explore` directories in headless mode | -| cypress:investigations:run:ess | Runs all tests tagged as ESS in the `e2e/investigations` directory in headless mode | +| cypress:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e` directory excluding `investigations`, `explore` and `rule_management` directories in headless mode | +| cypress:rule_management:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e/detection_response/rule_management` excluding `e2e/detection_response/rule_management/prebuilt_rules` directory in headless mode | +| cypress:rule_management:prebuilt_rules:run:serverless | Runs all tests tagged as ESS in the `e2e/detection_response/rule_management/prebuilt_rules` directory in headless mode | +| cypress:investigations:run:ess | Runs all tests tagged as SERVERLESS in the `e2e/investigations` directory in headless mode | | cypress:explore:run:ess | Runs all tests tagged as ESS in the `e2e/explore` directory in headless mode | | cypress:investigations:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e/investigations` directory in headless mode | | cypress:explore:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e/explore` directory in headless mode | | cypress:open:qa:serverless | Opens the Cypress UI with all tests in the `e2e` directory tagged as SERVERLESS. This also creates an MKI project in console.qa enviornment. The kibana instance will reload when you make code changes. This is the recommended way to debug tests in QA. Follow the readme in order to learn about the known limitations. | -| cypress:run:qa:serverless | Runs all tests tagged as SERVERLESS placed in the `e2e` directory excluding `investigations` and `explore` directories in headless mode using the QA environment and real MKI projects.| +| cypress:run:qa:serverless | Runs all tests tagged as SERVERLESS placed in the `e2e` directory excluding `investigations`, `explore` and `rule_management` directories in headless mode using the QA environment and real MKI projects.| | cypress:run:qa:serverless:explore | Runs all tests tagged as SERVERLESS in the `e2e/explore` directory in headless mode using the QA environment and real MKI prorjects. | | cypress:run:qa:serverless:investigations | Runs all tests tagged as SERVERLESS in the `e2e/investigations` directory in headless mode using the QA environment and reak MKI projects. | +| cypress:run:qa:serverless:rule_management | Runs all tests tagged as SERVERLESS in the `e2e/detection_response/rule_management` directory, excluding `e2e/detection_response/rule_management/prebuilt_rules` in headless mode using the QA environment and reak MKI projects. | +| cypress:run:qa:serverless:rule_management:prebuilt_rules | Runs all tests tagged as SERVERLESS in the `e2e/detection_response/rule_management/prebuilt_rules` directory in headless mode using the QA environment and reak MKI projects. | | junit:merge | Merges individual test reports into a single report and moves the report to the `junit` directory | Please note that all the headless mode commands do not open the Cypress UI and are typically used in CI/CD environments. The scripts that open the Cypress UI are useful for development and debugging. @@ -94,7 +100,7 @@ Below you can find the folder structure used on our Cypress tests. Cypress convention starting version 10 (previously known as integration). Contains the specs that are going to be executed. -### e2e/explore and e2e/investigations +### Area teams folders These directories contain tests which are run in their own Buildkite pipeline. @@ -103,7 +109,8 @@ If you belong to one of the teams listed in the table, please add new e2e specs | Directory | Area team | | -- | -- | | `e2e/explore` | Threat Hunting Explore | -| `e2e/investigations | Threat Hunting Investigations | +| `e2e/investigations` | Threat Hunting Investigations | +| `e2e/detection_response/rule_management` | Detection Rule Management | ### fixtures/ @@ -203,6 +210,8 @@ Run the tests with the following yarn scripts from `x-pack/test/security_solutio | cypress:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e` directory excluding `investigations` and `explore` directories in headless mode | | cypress:investigations:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e/investigations` directory in headless mode | | cypress:explore:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e/explore` directory in headless mode | +| cypress:rule_management:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e/detection_response/rule_management` excluding `e2e/detection_response/rule_management/prebuilt_rules` directory in headless mode | +| cypress:rule_management:prebuilt_rules:run:serverless | Runs all tests tagged as ESS in the `e2e/detection_response/rule_management/prebuilt_rules` directory in headless mode | Please note that all the headless mode commands do not open the Cypress UI and are typically used in CI/CD environments. The scripts that open the Cypress UI are useful for development and debugging. @@ -248,6 +257,8 @@ Run the tests with the following yarn scripts from `x-pack/test/security_solutio | cypress:run:qa:serverless | Runs all tests tagged as SERVERLESS placed in the `e2e` directory excluding `investigations` and `explore` directories in headless mode using the QA environment and real MKI projects.| | cypress:run:qa:serverless:explore | Runs all tests tagged as SERVERLESS in the `e2e/explore` directory in headless mode using the QA environment and real MKI prorjects. | | cypress:run:qa:serverless:investigations | Runs all tests tagged as SERVERLESS in the `e2e/investigations` directory in headless mode using the QA environment and reak MKI projects. | +| cypress:run:qa:serverless:rule_management | Runs all tests tagged as SERVERLESS in the `e2e/detection_response/rule_management` directory, excluding `e2e/detection_response/rule_management/prebuilt_rules` in headless mode using the QA environment and reak MKI projects. | +| cypress:run:qa:serverless:rule_management:prebuilt_rules | Runs all tests tagged as SERVERLESS in the `e2e/detection_response/rule_management/prebuilt_rules` directory in headless mode using the QA environment and reak MKI projects. | Please note that all the headless mode commands do not open the Cypress UI and are typically used in CI/CD environments. The scripts that open the Cypress UI are useful for development and debugging. diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/install_update_authorization.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_update_authorization.cy.ts similarity index 94% rename from x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/install_update_authorization.cy.ts rename to x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_update_authorization.cy.ts index e0078dd54e7ea..29e650dd4de66 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/install_update_authorization.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_update_authorization.cy.ts @@ -12,14 +12,14 @@ import { } from '@kbn/security-solution-plugin/common/constants'; import { ROLES } from '@kbn/security-solution-plugin/common/test'; -import { createRuleAssetSavedObject } from '../../../helpers/rules'; +import { createRuleAssetSavedObject } from '../../../../helpers/rules'; import { createAndInstallMockedPrebuiltRules, installPrebuiltRuleAssets, preventPrebuiltRulesPackageInstallation, -} from '../../../tasks/api_calls/prebuilt_rules'; -import { visit } from '../../../tasks/navigation'; -import { RULES_MANAGEMENT_URL } from '../../../urls/rules_management'; +} from '../../../../tasks/api_calls/prebuilt_rules'; +import { visit } from '../../../../tasks/navigation'; +import { RULES_MANAGEMENT_URL } from '../../../../urls/rules_management'; import { ADD_ELASTIC_RULES_BTN, getInstallSingleRuleButtonByRuleId, @@ -31,8 +31,8 @@ import { RULES_UPDATES_TAB, RULE_CHECKBOX, UPGRADE_ALL_RULES_BUTTON, -} from '../../../screens/alerts_detection_rules'; -import { login } from '../../../tasks/login'; +} from '../../../../screens/alerts_detection_rules'; +import { login } from '../../../../tasks/login'; // Rule to test update const RULE_1_ID = 'rule_1'; diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/install_update_error_handling.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_update_error_handling.cy.ts similarity index 94% rename from x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/install_update_error_handling.cy.ts rename to x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_update_error_handling.cy.ts index 7e288910ccb60..db84d92e4ddb6 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/install_update_error_handling.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_update_error_handling.cy.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { createRuleAssetSavedObject } from '../../../helpers/rules'; +import { createRuleAssetSavedObject } from '../../../../helpers/rules'; import { getInstallSingleRuleButtonByRuleId, getUpgradeSingleRuleButtonByRuleId, @@ -14,14 +14,14 @@ import { SELECT_ALL_RULES_ON_PAGE_CHECKBOX, UPGRADE_ALL_RULES_BUTTON, UPGRADE_SELECTED_RULES_BUTTON, -} from '../../../screens/alerts_detection_rules'; -import { selectRulesByName } from '../../../tasks/alerts_detection_rules'; +} from '../../../../screens/alerts_detection_rules'; +import { selectRulesByName } from '../../../../tasks/alerts_detection_rules'; import { installPrebuiltRuleAssets, createAndInstallMockedPrebuiltRules, preventPrebuiltRulesPackageInstallation, -} from '../../../tasks/api_calls/prebuilt_rules'; -import { login } from '../../../tasks/login'; +} from '../../../../tasks/api_calls/prebuilt_rules'; +import { login } from '../../../../tasks/login'; import { clickAddElasticRulesButton, assertInstallationRequestIsComplete, @@ -33,8 +33,8 @@ import { assertRulesPresentInAddPrebuiltRulesTable, assertRuleUpgradeFailureToastShown, assertRulesPresentInRuleUpdatesTable, -} from '../../../tasks/prebuilt_rules'; -import { visitRulesManagementTable } from '../../../tasks/rules_management'; +} from '../../../../tasks/prebuilt_rules'; +import { visitRulesManagementTable } from '../../../../tasks/rules_management'; describe( 'Detection rules, Prebuilt Rules Installation and Update - Error handling', diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/install_via_fleet.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_via_fleet.cy.ts similarity index 90% rename from x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/install_via_fleet.cy.ts rename to x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_via_fleet.cy.ts index 6da3d58c0530d..762e79bb27003 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/install_via_fleet.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_via_fleet.cy.ts @@ -8,13 +8,13 @@ import type { BulkInstallPackageInfo } from '@kbn/fleet-plugin/common'; import type { Rule } from '@kbn/security-solution-plugin/public/detection_engine/rule_management/logic/types'; -import { resetRulesTableState } from '../../../tasks/common'; -import { INSTALL_ALL_RULES_BUTTON, TOASTER } from '../../../screens/alerts_detection_rules'; -import { getRuleAssets } from '../../../tasks/api_calls/prebuilt_rules'; -import { login } from '../../../tasks/login'; -import { clickAddElasticRulesButton } from '../../../tasks/prebuilt_rules'; -import { visitRulesManagementTable } from '../../../tasks/rules_management'; -import { deleteAlertsAndRules } from '../../../tasks/api_calls/common'; +import { resetRulesTableState } from '../../../../tasks/common'; +import { INSTALL_ALL_RULES_BUTTON, TOASTER } from '../../../../screens/alerts_detection_rules'; +import { getRuleAssets } from '../../../../tasks/api_calls/prebuilt_rules'; +import { login } from '../../../../tasks/login'; +import { clickAddElasticRulesButton } from '../../../../tasks/prebuilt_rules'; +import { visitRulesManagementTable } from '../../../../tasks/rules_management'; +import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common'; describe( 'Detection rules, Prebuilt Rules Installation and Update workflow', diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/install_workflow.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_workflow.cy.ts similarity index 85% rename from x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/install_workflow.cy.ts rename to x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_workflow.cy.ts index ec4615bcf59e4..523d0ec0ad4e0 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/install_workflow.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_workflow.cy.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { resetRulesTableState } from '../../../tasks/common'; -import { createRuleAssetSavedObject } from '../../../helpers/rules'; +import { resetRulesTableState } from '../../../../tasks/common'; +import { createRuleAssetSavedObject } from '../../../../helpers/rules'; import { getInstallSingleRuleButtonByRuleId, GO_BACK_TO_RULES_TABLE_BUTTON, @@ -16,19 +16,19 @@ import { RULE_CHECKBOX, SELECT_ALL_RULES_ON_PAGE_CHECKBOX, TOASTER, -} from '../../../screens/alerts_detection_rules'; -import { selectRulesByName } from '../../../tasks/alerts_detection_rules'; -import { RULE_MANAGEMENT_PAGE_BREADCRUMB } from '../../../screens/breadcrumbs'; -import { installPrebuiltRuleAssets } from '../../../tasks/api_calls/prebuilt_rules'; -import { login } from '../../../tasks/login'; +} from '../../../../screens/alerts_detection_rules'; +import { selectRulesByName } from '../../../../tasks/alerts_detection_rules'; +import { RULE_MANAGEMENT_PAGE_BREADCRUMB } from '../../../../screens/breadcrumbs'; +import { installPrebuiltRuleAssets } from '../../../../tasks/api_calls/prebuilt_rules'; +import { login } from '../../../../tasks/login'; import { assertInstallationRequestIsComplete, assertRuleInstallationSuccessToastShown, assertRulesPresentInInstalledRulesTable, clickAddElasticRulesButton, -} from '../../../tasks/prebuilt_rules'; -import { visitRulesManagementTable } from '../../../tasks/rules_management'; -import { deleteAlertsAndRules } from '../../../tasks/api_calls/common'; +} from '../../../../tasks/prebuilt_rules'; +import { visitRulesManagementTable } from '../../../../tasks/rules_management'; +import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common'; describe( 'Detection rules, Prebuilt Rules Installation and Update workflow', diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/management.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/management.cy.ts similarity index 91% rename from x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/management.cy.ts rename to x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/management.cy.ts index f3101f513915f..15e020b5e0663 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/management.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/management.cy.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { createRuleAssetSavedObject } from '../../../helpers/rules'; +import { createRuleAssetSavedObject } from '../../../../helpers/rules'; import { COLLAPSED_ACTION_BTN, ELASTIC_RULES_BTN, @@ -15,7 +15,7 @@ import { RULE_SWITCH, SELECT_ALL_RULES_ON_PAGE_CHECKBOX, INSTALL_ALL_RULES_BUTTON, -} from '../../../screens/alerts_detection_rules'; +} from '../../../../screens/alerts_detection_rules'; import { deleteFirstRule, disableAutoRefresh, @@ -24,21 +24,24 @@ import { selectRulesByName, waitForPrebuiltDetectionRulesToBeLoaded, waitForRuleToUpdate, -} from '../../../tasks/alerts_detection_rules'; +} from '../../../../tasks/alerts_detection_rules'; import { deleteSelectedRules, disableSelectedRules, enableSelectedRules, -} from '../../../tasks/rules_bulk_actions'; +} from '../../../../tasks/rules_bulk_actions'; import { createAndInstallMockedPrebuiltRules, getAvailablePrebuiltRulesCount, preventPrebuiltRulesPackageInstallation, -} from '../../../tasks/api_calls/prebuilt_rules'; -import { deleteAlertsAndRules, deletePrebuiltRulesAssets } from '../../../tasks/api_calls/common'; -import { login } from '../../../tasks/login'; -import { visit } from '../../../tasks/navigation'; -import { RULES_MANAGEMENT_URL } from '../../../urls/rules_management'; +} from '../../../../tasks/api_calls/prebuilt_rules'; +import { + deleteAlertsAndRules, + deletePrebuiltRulesAssets, +} from '../../../../tasks/api_calls/common'; +import { login } from '../../../../tasks/login'; +import { visit } from '../../../../tasks/navigation'; +import { RULES_MANAGEMENT_URL } from '../../../../urls/rules_management'; const rules = Array.from(Array(5)).map((_, i) => { return createRuleAssetSavedObject({ diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/notifications.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/notifications.cy.ts similarity index 92% rename from x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/notifications.cy.ts rename to x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/notifications.cy.ts index 92bf9e7f1471c..4812efc740ae2 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/notifications.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/notifications.cy.ts @@ -5,22 +5,25 @@ * 2.0. */ -import { createRuleAssetSavedObject } from '../../../helpers/rules'; +import { createRuleAssetSavedObject } from '../../../../helpers/rules'; import { ADD_ELASTIC_RULES_BTN, ADD_ELASTIC_RULES_EMPTY_PROMPT_BTN, RULES_UPDATES_TAB, -} from '../../../screens/alerts_detection_rules'; -import { deleteFirstRule } from '../../../tasks/alerts_detection_rules'; -import { deleteAlertsAndRules, deletePrebuiltRulesAssets } from '../../../tasks/api_calls/common'; +} from '../../../../screens/alerts_detection_rules'; +import { deleteFirstRule } from '../../../../tasks/alerts_detection_rules'; +import { + deleteAlertsAndRules, + deletePrebuiltRulesAssets, +} from '../../../../tasks/api_calls/common'; import { installAllPrebuiltRulesRequest, installPrebuiltRuleAssets, createAndInstallMockedPrebuiltRules, -} from '../../../tasks/api_calls/prebuilt_rules'; -import { resetRulesTableState } from '../../../tasks/common'; -import { login } from '../../../tasks/login'; -import { visitRulesManagementTable } from '../../../tasks/rules_management'; +} from '../../../../tasks/api_calls/prebuilt_rules'; +import { resetRulesTableState } from '../../../../tasks/common'; +import { login } from '../../../../tasks/login'; +import { visitRulesManagementTable } from '../../../../tasks/rules_management'; const RULE_1 = createRuleAssetSavedObject({ name: 'Test rule 1', diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/prebuilt_rules_preview.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/prebuilt_rules_preview.cy.ts similarity index 97% rename from x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/prebuilt_rules_preview.cy.ts rename to x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/prebuilt_rules_preview.cy.ts index 6deeb6f5202c0..81f37b7760df2 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/prebuilt_rules_preview.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/prebuilt_rules_preview.cy.ts @@ -12,22 +12,22 @@ import type { PrebuiltRuleAsset } from '@kbn/security-solution-plugin/server/lib import type { Threshold } from '@kbn/security-solution-plugin/common/api/detection_engine/model/rule_schema'; import { AlertSuppression } from '@kbn/security-solution-plugin/common/api/detection_engine/model/rule_schema'; -import { createRuleAssetSavedObject } from '../../../helpers/rules'; +import { createRuleAssetSavedObject } from '../../../../helpers/rules'; import { INSTALL_PREBUILT_RULE_BUTTON, INSTALL_PREBUILT_RULE_PREVIEW, UPDATE_PREBUILT_RULE_PREVIEW, UPDATE_PREBUILT_RULE_BUTTON, -} from '../../../screens/alerts_detection_rules'; -import { RULE_MANAGEMENT_PAGE_BREADCRUMB } from '../../../screens/breadcrumbs'; +} from '../../../../screens/alerts_detection_rules'; +import { RULE_MANAGEMENT_PAGE_BREADCRUMB } from '../../../../screens/breadcrumbs'; import { installPrebuiltRuleAssets, createAndInstallMockedPrebuiltRules, -} from '../../../tasks/api_calls/prebuilt_rules'; -import { createSavedQuery, deleteSavedQueries } from '../../../tasks/api_calls/saved_queries'; -import { fetchMachineLearningModules } from '../../../tasks/api_calls/machine_learning'; -import { resetRulesTableState } from '../../../tasks/common'; -import { login } from '../../../tasks/login'; +} from '../../../../tasks/api_calls/prebuilt_rules'; +import { createSavedQuery, deleteSavedQueries } from '../../../../tasks/api_calls/saved_queries'; +import { fetchMachineLearningModules } from '../../../../tasks/api_calls/machine_learning'; +import { resetRulesTableState } from '../../../../tasks/common'; +import { login } from '../../../../tasks/login'; import { assertRuleInstallationSuccessToastShown, assertRulesNotPresentInAddPrebuiltRulesTable, @@ -36,7 +36,7 @@ import { assertRuleUpgradeSuccessToastShown, clickAddElasticRulesButton, clickRuleUpdatesTab, -} from '../../../tasks/prebuilt_rules'; +} from '../../../../tasks/prebuilt_rules'; import { assertAlertSuppressionPropertiesShown, assertCommonPropertiesShown, @@ -55,13 +55,13 @@ import { closeRulePreview, openRuleInstallPreview, openRuleUpdatePreview, -} from '../../../tasks/prebuilt_rules_preview'; -import { visitRulesManagementTable } from '../../../tasks/rules_management'; +} from '../../../../tasks/prebuilt_rules_preview'; +import { visitRulesManagementTable } from '../../../../tasks/rules_management'; import { deleteAlertsAndRules, deleteDataView, postDataView, -} from '../../../tasks/api_calls/common'; +} from '../../../../tasks/api_calls/common'; const TEST_ENV_TAGS = ['@ess', '@serverless']; diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/update_workflow.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/update_workflow.ts similarity index 88% rename from x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/update_workflow.ts rename to x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/update_workflow.ts index edeb8ac98623b..d858280dd5294 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/update_workflow.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/update_workflow.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { createRuleAssetSavedObject } from '../../../helpers/rules'; +import { createRuleAssetSavedObject } from '../../../../helpers/rules'; import { getUpgradeSingleRuleButtonByRuleId, NO_RULES_AVAILABLE_FOR_UPGRADE_MESSAGE, @@ -13,22 +13,22 @@ import { SELECT_ALL_RULES_ON_PAGE_CHECKBOX, UPGRADE_ALL_RULES_BUTTON, UPGRADE_SELECTED_RULES_BUTTON, -} from '../../../screens/alerts_detection_rules'; -import { selectRulesByName } from '../../../tasks/alerts_detection_rules'; -import { deleteAlertsAndRules } from '../../../tasks/api_calls/common'; +} from '../../../../screens/alerts_detection_rules'; +import { selectRulesByName } from '../../../../tasks/alerts_detection_rules'; +import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common'; import { installPrebuiltRuleAssets, createAndInstallMockedPrebuiltRules, -} from '../../../tasks/api_calls/prebuilt_rules'; -import { resetRulesTableState } from '../../../tasks/common'; -import { login } from '../../../tasks/login'; +} from '../../../../tasks/api_calls/prebuilt_rules'; +import { resetRulesTableState } from '../../../../tasks/common'; +import { login } from '../../../../tasks/login'; import { assertRulesNotPresentInRuleUpdatesTable, assertRuleUpgradeSuccessToastShown, assertUpgradeRequestIsComplete, clickRuleUpdatesTab, -} from '../../../tasks/prebuilt_rules'; -import { visitRulesManagementTable } from '../../../tasks/rules_management'; +} from '../../../../tasks/prebuilt_rules'; +import { visitRulesManagementTable } from '../../../../tasks/rules_management'; describe( 'Detection rules, Prebuilt Rules Installation and Update workflow', diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_details/common_flows.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/common_flows.cy.ts similarity index 88% rename from x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_details/common_flows.cy.ts rename to x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/common_flows.cy.ts index f5704122d9e33..0610786fc1b89 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_details/common_flows.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/common_flows.cy.ts @@ -5,17 +5,17 @@ * 2.0. */ -import { deleteRuleFromDetailsPage } from '../../../tasks/alerts_detection_rules'; +import { deleteRuleFromDetailsPage } from '../../../../tasks/alerts_detection_rules'; import { CUSTOM_RULES_BTN, RULES_MANAGEMENT_TABLE, RULES_ROW, -} from '../../../screens/alerts_detection_rules'; -import { createRule } from '../../../tasks/api_calls/rules'; -import { getDetails } from '../../../tasks/rule_details'; -import { ruleFields } from '../../../data/detection_engine'; -import { getTimeline } from '../../../objects/timeline'; -import { getExistingRule, getNewRule } from '../../../objects/rule'; +} from '../../../../screens/alerts_detection_rules'; +import { createRule } from '../../../../tasks/api_calls/rules'; +import { getDetails } from '../../../../tasks/rule_details'; +import { ruleFields } from '../../../../data/detection_engine'; +import { getTimeline } from '../../../../objects/timeline'; +import { getExistingRule, getNewRule } from '../../../../objects/rule'; import { ABOUT_DETAILS, @@ -42,13 +42,13 @@ import { THREAT_TACTIC, THREAT_TECHNIQUE, TIMELINE_TEMPLATE_DETAILS, -} from '../../../screens/rule_details'; +} from '../../../../screens/rule_details'; -import { createTimeline } from '../../../tasks/api_calls/timelines'; -import { deleteAlertsAndRules, deleteConnectors } from '../../../tasks/api_calls/common'; -import { login } from '../../../tasks/login'; -import { visit } from '../../../tasks/navigation'; -import { ruleDetailsUrl } from '../../../urls/rule_details'; +import { createTimeline } from '../../../../tasks/api_calls/timelines'; +import { deleteAlertsAndRules, deleteConnectors } from '../../../../tasks/api_calls/common'; +import { login } from '../../../../tasks/login'; +import { visit } from '../../../../tasks/navigation'; +import { ruleDetailsUrl } from '../../../../urls/rule_details'; // This test is meant to test all common aspects of the rule details page that should function // the same regardless of rule type. For any rule type specific functionalities, please include diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_details/esql_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/esql_rule.cy.ts similarity index 69% rename from x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_details/esql_rule.cy.ts rename to x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/esql_rule.cy.ts index 7d1419e911e33..c59b7db55c743 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_details/esql_rule.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/esql_rule.cy.ts @@ -5,24 +5,24 @@ * 2.0. */ -import { getEsqlRule } from '../../../objects/rule'; +import { getEsqlRule } from '../../../../objects/rule'; import { ESQL_QUERY_DETAILS, DEFINITION_DETAILS, RULE_NAME_HEADER, RULE_TYPE_DETAILS, -} from '../../../screens/rule_details'; +} from '../../../../screens/rule_details'; -import { createRule } from '../../../tasks/api_calls/rules'; +import { createRule } from '../../../../tasks/api_calls/rules'; -import { getDetails } from '../../../tasks/rule_details'; -import { deleteAlertsAndRules } from '../../../tasks/api_calls/common'; +import { getDetails } from '../../../../tasks/rule_details'; +import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common'; -import { login } from '../../../tasks/login'; -import { visit } from '../../../tasks/navigation'; +import { login } from '../../../../tasks/login'; +import { visit } from '../../../../tasks/navigation'; -import { ruleDetailsUrl } from '../../../urls/rule_details'; +import { ruleDetailsUrl } from '../../../../urls/rule_details'; describe('Detection ES|QL rules, details view', { tags: ['@ess'] }, () => { const rule = getEsqlRule(); diff --git a/x-pack/test/security_solution_cypress/package.json b/x-pack/test/security_solution_cypress/package.json index e43f32a447575..e1f552fdba9de 100644 --- a/x-pack/test/security_solution_cypress/package.json +++ b/x-pack/test/security_solution_cypress/package.json @@ -7,9 +7,11 @@ "scripts": { "cypress": "NODE_OPTIONS=--openssl-legacy-provider ../../../node_modules/.bin/cypress", "cypress:open:ess": "TZ=UTC NODE_OPTIONS=--openssl-legacy-provider node ../../plugins/security_solution/scripts/start_cypress_parallel open --spec './cypress/e2e/**/*.cy.ts' --config-file ../../test/security_solution_cypress/cypress/cypress.config.ts --ftr-config-file ../../test/security_solution_cypress/cli_config", - "cypress:run:ess": "yarn cypress:ess --spec './cypress/e2e/!(investigations|explore)/**/*.cy.ts'", + "cypress:run:ess": "yarn cypress:ess --spec './cypress/e2e/!(investigations|explore|detection_response/rule_management)/**/*.cy.ts'", "cypress:run:cases:ess": "yarn cypress:ess --spec './cypress/e2e/explore/cases/*.cy.ts'", "cypress:ess": "TZ=UTC NODE_OPTIONS=--openssl-legacy-provider node ../../plugins/security_solution/scripts/start_cypress_parallel run --config-file ../../test/security_solution_cypress/cypress/cypress_ci.config.ts --ftr-config-file ../../test/security_solution_cypress/cli_config", + "cypress:rule_management:run:ess":"yarn cypress:ess --spec './cypress/e2e/detection_response/rule_management/!(prebuilt_rules)/**/*.cy.ts'", + "cypress:rule_management:prebuilt_rules:run:ess": "yarn cypress:ess --spec './cypress/e2e/detection_response/rule_management/prebuilt_rules/**/*.cy.ts'", "cypress:run:respops:ess": "yarn cypress:ess --spec './cypress/e2e/(detection_response|exceptions)/**/*.cy.ts'", "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'", @@ -21,16 +23,20 @@ "cypress:cloud:serverless": "TZ=UTC NODE_OPTIONS=--openssl-legacy-provider NODE_TLS_REJECT_UNAUTHORIZED=0 ../../../node_modules/.bin/cypress", "cypress:open:cloud:serverless": "yarn cypress:cloud:serverless open --config-file ./cypress/cypress_serverless.config.ts --env CLOUD_SERVERLESS=true", "cypress:open:serverless": "yarn cypress:serverless open --config-file ../../test/security_solution_cypress/cypress/cypress_serverless.config.ts --spec './cypress/e2e/**/*.cy.ts'", - "cypress:run:serverless": "yarn cypress:serverless --spec './cypress/e2e/!(investigations|explore)/**/*.cy.ts'", + "cypress:run:serverless": "yarn cypress:serverless --spec './cypress/e2e/!(investigations|explore|detection_response/rule_management)/**/*.cy.ts'", "cypress:run:cloud:serverless": "yarn cypress:cloud:serverless run --config-file ./cypress/cypress_ci_serverless.config.ts --env CLOUD_SERVERLESS=true", + "cypress:rule_management:run:serverless": "yarn cypress:serverless --spec './cypress/e2e/detection_response/rule_management/!(prebuilt_rules)/**/*.cy.ts'", + "cypress:rule_management:prebuilt_rules:run:serverless": "yarn cypress:serverless --spec './cypress/e2e/detection_response/rule_management/prebuilt_rules/**/*.cy.ts'", "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: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", - "cypress:run:qa:serverless": "yarn cypress:qa:serverless --spec './cypress/e2e/!(investigations|explore)/**/*.cy.ts'", + "cypress:run:qa:serverless": "yarn cypress:qa:serverless --spec './cypress/e2e/!(investigations|explore|detection_response/rule_management)/**/*.cy.ts'", "cypress:run:qa:serverless:investigations": "yarn cypress:qa:serverless --spec './cypress/e2e/investigations/**/*.cy.ts'", - "cypress:run:qa:serverless:explore": "yarn cypress:qa:serverless --spec './cypress/e2e/explore/**/*.cy.ts'" + "cypress:run:qa:serverless:explore": "yarn cypress:qa:serverless --spec './cypress/e2e/explore/**/*.cy.ts'", + "cypress:run:qa:serverless:rule_management": "yarn cypress:qa:serverless --spec './cypress/e2e/detection_response/rule_management/!(prebuilt_rules)/**/*.cy.ts'", + "cypress:run:qa:serverless:rule_management:prebuilt_rules": "yarn cypress:qa:serverless --spec './cypress/e2e/detection_response/rule_management/prebuilt_rules/**/*.cy.ts'" } } \ No newline at end of file From 36c86fc532c1fec5fdd8583805862b35349ed280 Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Tue, 28 Nov 2023 14:38:50 +0100 Subject: [PATCH 12/14] [cloud_security_posture_functional] fix functional tests (#171736) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Some async functions miss `await` that makes CI to fail https://buildkite.com/elastic/kibana-pull-request/builds/177811#018bf56a-125a-448d-b3bb-e9da9b2c512a ``` 2023-11-22 06:36:50 CEST | 44 passing (9.0m) -- | --   | 2023-11-22 06:36:50 CEST | 1 pending   | 2023-11-22 06:36:50 CEST |     | 2023-11-22 06:36:50 CEST | warn browser[SEVERE] ERROR FETCHING BROWSR LOGS: This driver instance does not have a valid session ID (did you call WebDriver.quit()?) and may no longer be used.   | 2023-11-22 06:36:50 CEST | Unhandled Promise rejection detected:   | 2023-11-22 06:36:50 CEST |     | 2023-11-22 06:36:50 CEST | NoSuchSessionError: This driver instance does not have a valid session ID (did you call WebDriver.quit()?) and may no longer be used.   | 2023-11-22 06:36:50 CEST | at /var/lib/buildkite-agent/builds/kb-n2-4-spot-da643dd7dfd9a4eb/elastic/kibana-pull-request/kibana/node_modules/selenium-webdriver/lib/webdriver.js:776:9   | 2023-11-22 06:36:50 CEST | at Object.thenFinally [as finally] (/var/lib/buildkite-agent/builds/kb-n2-4-spot-da643dd7dfd9a4eb/elastic/kibana-pull-request/kibana/node_modules/selenium-webdriver/lib/promise.js:101:12)   | 2023-11-22 06:36:50 CEST | at processTicksAndRejections (node:internal/process/task_queues:95:5)   | 2023-11-22 06:36:50 CEST | at remote.ts:101:7   | 2023-11-22 06:36:50 CEST | at tryWebDriverCall (remote.ts:34:7)   | 2023-11-22 06:36:50 CEST | at remote.ts:100:5   | 2023-11-22 06:36:50 CEST | at lifecycle_phase.ts:76:11   | 2023-11-22 06:36:50 CEST | at async Promise.all (index 2)   | 2023-11-22 06:36:50 CEST | at LifecyclePhase.trigger (lifecycle_phase.ts:73:5)   | 2023-11-22 06:36:50 CEST | at FunctionalTestRunner.runHarness (functional_test_runner.ts:258:9)   | 2023-11-22 06:36:50 CEST | at FunctionalTestRunner.run (functional_test_runner.ts:48:12)   | 2023-11-22 06:36:50 CEST | at runFtr (run_ftr.ts:21:24)   | 2023-11-22 06:36:50 CEST | at run_tests.ts:116:11   | 2023-11-22 06:36:50 CEST | at withProcRunner (with_proc_runner.ts:29:5)   | 2023-11-22 06:36:50 CEST | at run_tests.ts:87:7   | 2023-11-22 06:36:50 CEST | at tooling_log.ts:84:18   | 2023-11-22 06:36:50 CEST | at runTests (run_tests.ts:64:5)   | 2023-11-22 06:36:50 CEST | at description (cli.ts:24:7)   | 2023-11-22 06:36:50 CEST | at run.ts:73:10   | 2023-11-22 06:36:50 CEST | at withProcRunner (with_proc_runner.ts:29:5)   | 2023-11-22 06:36:50 CEST | at run (run.ts:71:5) {   | 2023-11-22 06:36:50 CEST | remoteStacktrace: '' ``` --- .../pages/findings_old_data.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/test/cloud_security_posture_functional/pages/findings_old_data.ts b/x-pack/test/cloud_security_posture_functional/pages/findings_old_data.ts index a8cda10482e2e..1269c85c1ecda 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/findings_old_data.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/findings_old_data.ts @@ -10,7 +10,7 @@ import Chance from 'chance'; import type { FtrProviderContext } from '../ftr_provider_context'; // eslint-disable-next-line import/no-default-export -export default function ({ getPageObjects, getService }: FtrProviderContext) { +export default function ({ getPageObjects }: FtrProviderContext) { const pageObjects = getPageObjects(['common', 'findings', 'header']); const chance = new Chance(); const hoursToMillisecond = (hours: number) => hours * 60 * 60 * 1000; @@ -77,7 +77,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await findings.index.add(dataOldKspm); await findings.navigateToLatestFindingsPage(); - pageObjects.header.waitUntilLoadingHasFinished(); + await pageObjects.header.waitUntilLoadingHasFinished(); expect(await findings.isLatestFindingsTableThere()).to.be(false); }); it('returns no Findings CSPM', async () => { @@ -86,7 +86,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await findings.index.add(dataOldCspm); await findings.navigateToLatestFindingsPage(); - pageObjects.header.waitUntilLoadingHasFinished(); + await pageObjects.header.waitUntilLoadingHasFinished(); expect(await findings.isLatestFindingsTableThere()).to.be(false); }); }); From 177dbd1da5900ceb8c3fbd9efa27362361963fff Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Tue, 28 Nov 2023 08:00:28 -0600 Subject: [PATCH 13/14] Revert "[Security Solution] Specific Cypress executions for `Rule Management` team (#171868)" This reverts commit 242cb6f1d5cf97766fe8a5697488877e2390befe. --- .../verify_es_serverless_image.yml | 26 ---------- .buildkite/pipelines/on_merge.yml | 48 ------------------- .buildkite/pipelines/pull_request/base.yml | 48 ------------------- .../security_solution_cypress.yml | 24 ---------- .../security_serverless_rule_management.sh | 16 ------- ...rverless_rule_management_prebuilt_rules.sh | 16 ------- .../security_solution_rule_management.sh | 16 ------- ...solution_rule_management_prebuilt_rules.sh | 16 ------- .github/CODEOWNERS | 2 + .../cypress/README.md | 23 +++------ .../install_update_authorization.cy.ts | 12 ++--- .../install_update_error_handling.cy.ts | 14 +++--- .../prebuilt_rules/install_via_fleet.cy.ts | 14 +++--- .../prebuilt_rules/install_workflow.cy.ts | 20 ++++---- .../prebuilt_rules/management.cy.ts | 21 ++++---- .../prebuilt_rules/notifications.cy.ts | 19 ++++---- .../prebuilt_rules_preview.cy.ts | 24 +++++----- .../prebuilt_rules/update_workflow.ts | 18 +++---- .../rule_details/common_flows.cy.ts | 26 +++++----- .../rule_details/esql_rule.cy.ts | 16 +++---- .../security_solution_cypress/package.json | 14 ++---- 21 files changed, 101 insertions(+), 332 deletions(-) delete mode 100644 .buildkite/scripts/steps/functional/security_serverless_rule_management.sh delete mode 100644 .buildkite/scripts/steps/functional/security_serverless_rule_management_prebuilt_rules.sh delete mode 100644 .buildkite/scripts/steps/functional/security_solution_rule_management.sh delete mode 100644 .buildkite/scripts/steps/functional/security_solution_rule_management_prebuilt_rules.sh rename x-pack/test/security_solution_cypress/cypress/e2e/detection_response/{rule_management => }/prebuilt_rules/install_update_authorization.cy.ts (94%) rename x-pack/test/security_solution_cypress/cypress/e2e/detection_response/{rule_management => }/prebuilt_rules/install_update_error_handling.cy.ts (94%) rename x-pack/test/security_solution_cypress/cypress/e2e/detection_response/{rule_management => }/prebuilt_rules/install_via_fleet.cy.ts (90%) rename x-pack/test/security_solution_cypress/cypress/e2e/detection_response/{rule_management => }/prebuilt_rules/install_workflow.cy.ts (85%) rename x-pack/test/security_solution_cypress/cypress/e2e/detection_response/{rule_management => }/prebuilt_rules/management.cy.ts (91%) rename x-pack/test/security_solution_cypress/cypress/e2e/detection_response/{rule_management => }/prebuilt_rules/notifications.cy.ts (92%) rename x-pack/test/security_solution_cypress/cypress/e2e/detection_response/{rule_management => }/prebuilt_rules/prebuilt_rules_preview.cy.ts (97%) rename x-pack/test/security_solution_cypress/cypress/e2e/detection_response/{rule_management => }/prebuilt_rules/update_workflow.ts (88%) rename x-pack/test/security_solution_cypress/cypress/e2e/detection_response/{rule_management => }/rule_details/common_flows.cy.ts (88%) rename x-pack/test/security_solution_cypress/cypress/e2e/detection_response/{rule_management => }/rule_details/esql_rule.cy.ts (69%) diff --git a/.buildkite/pipelines/es_serverless/verify_es_serverless_image.yml b/.buildkite/pipelines/es_serverless/verify_es_serverless_image.yml index 8d1b778b67983..8e64513b14900 100644 --- a/.buildkite/pipelines/es_serverless/verify_es_serverless_image.yml +++ b/.buildkite/pipelines/es_serverless/verify_es_serverless_image.yml @@ -95,32 +95,6 @@ steps: - exit_status: '*' limit: 1 - - command: .buildkite/scripts/steps/functional/security_serverless_rule_management.sh - label: 'Serverless Rule Management - Security Solution Cypress Tests' - if: "build.env('SKIP_CYPRESS') != '1' && build.env('SKIP_CYPRESS') != 'true'" - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 60 - parallelism: 8 - retry: - automatic: - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/security_serverless_rule_management_prebuilt_rules.sh - label: 'Serverless Rule Management - Prebuilt Rules - Security Solution Cypress Tests' - if: "build.env('SKIP_CYPRESS') != '1' && build.env('SKIP_CYPRESS') != 'true'" - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 60 - parallelism: 6 - retry: - automatic: - - exit_status: '*' - limit: 1 - - command: .buildkite/scripts/steps/functional/defend_workflows_serverless.sh label: 'Defend Workflows Cypress Tests on Serverless' if: "build.env('SKIP_CYPRESS') != '1' && build.env('SKIP_CYPRESS') != 'true'" diff --git a/.buildkite/pipelines/on_merge.yml b/.buildkite/pipelines/on_merge.yml index 8256eb2395633..8b00db428a713 100644 --- a/.buildkite/pipelines/on_merge.yml +++ b/.buildkite/pipelines/on_merge.yml @@ -115,54 +115,6 @@ steps: - exit_status: '*' limit: 1 - - command: .buildkite/scripts/steps/functional/security_serverless_rule_management.sh - label: 'Serverless Rule Management - Security Solution Cypress Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 60 - parallelism: 8 - retry: - automatic: - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/security_serverless_rule_management_prebuilt_rules.sh - label: 'Serverless Rule Management - Prebuilt Rules - Security Solution Cypress Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 60 - parallelism: 6 - retry: - automatic: - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/security_solution_rule_management.sh - label: 'Rule Management - Security Solution Cypress Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 60 - parallelism: 8 - retry: - automatic: - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/security_solution_rule_management_prebuilt_rules.sh - label: 'Rule Management - Prebuilt Rules - Security Solution Cypress Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 60 - parallelism: 6 - retry: - automatic: - - exit_status: '*' - limit: 1 - - command: .buildkite/scripts/steps/functional/security_solution.sh label: 'Security Solution Cypress Tests' agents: diff --git a/.buildkite/pipelines/pull_request/base.yml b/.buildkite/pipelines/pull_request/base.yml index 8238afbee4fd2..49215bbd00f11 100644 --- a/.buildkite/pipelines/pull_request/base.yml +++ b/.buildkite/pipelines/pull_request/base.yml @@ -93,30 +93,6 @@ steps: - exit_status: '*' limit: 1 - - command: .buildkite/scripts/steps/functional/security_serverless_rule_management.sh - label: 'Serverless Rule Management - Security Solution Cypress Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 60 - parallelism: 8 - retry: - automatic: - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/security_serverless_rule_management_prebuilt_rules.sh - label: 'Serverless Rule Management - Prebuilt Rules - Security Solution Cypress Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 60 - parallelism: 6 - retry: - automatic: - - exit_status: '*' - limit: 1 - - command: .buildkite/scripts/steps/functional/security_solution.sh label: 'Security Solution Cypress Tests' agents: @@ -141,30 +117,6 @@ steps: - exit_status: '*' limit: 1 - - command: .buildkite/scripts/steps/functional/security_solution_rule_management.sh - label: 'Rule Management - Security Solution Cypress Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 60 - parallelism: 8 - retry: - automatic: - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/security_solution_rule_management_prebuilt_rules.sh - label: 'Rule Management - Prebuilt Rules - Security Solution Cypress Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 60 - parallelism: 6 - retry: - automatic: - - exit_status: '*' - limit: 1 - - command: .buildkite/scripts/steps/functional/security_solution_investigations.sh label: 'Investigations - Security Solution Cypress Tests' agents: diff --git a/.buildkite/pipelines/security_solution/security_solution_cypress.yml b/.buildkite/pipelines/security_solution/security_solution_cypress.yml index 77e7fea574352..247505ef1c85a 100644 --- a/.buildkite/pipelines/security_solution/security_solution_cypress.yml +++ b/.buildkite/pipelines/security_solution/security_solution_cypress.yml @@ -30,30 +30,6 @@ steps: # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. timeout_in_minutes: 300 parallelism: 8 - retry: - automatic: - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:rule_management - label: 'Serverless MKI QA Rule Management - Security Solution Cypress Tests' - agents: - queue: n2-4-spot - # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. - timeout_in_minutes: 300 - parallelism: 8 - retry: - automatic: - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:rule_management:prebuilt_rules - label: 'Serverless MKI QA Rule Management - Prebuilt Rules - Security Solution Cypress Tests' - agents: - queue: n2-4-spot - # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. - timeout_in_minutes: 300 - parallelism: 6 retry: automatic: - exit_status: '*' diff --git a/.buildkite/scripts/steps/functional/security_serverless_rule_management.sh b/.buildkite/scripts/steps/functional/security_serverless_rule_management.sh deleted file mode 100644 index 5d360e0db4f29..0000000000000 --- a/.buildkite/scripts/steps/functional/security_serverless_rule_management.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/steps/functional/common.sh -source .buildkite/scripts/steps/functional/common_cypress.sh - -export JOB=kibana-security-solution-chrome -export KIBANA_INSTALL_DIR=${KIBANA_BUILD_LOCATION} - -echo "--- Rule Management Cypress Tests on Serverless" - -cd x-pack/test/security_solution_cypress - -set +e -yarn cypress:rule_management:run:serverless; status=$?; yarn junit:merge || :; exit $status diff --git a/.buildkite/scripts/steps/functional/security_serverless_rule_management_prebuilt_rules.sh b/.buildkite/scripts/steps/functional/security_serverless_rule_management_prebuilt_rules.sh deleted file mode 100644 index bc7dc3269d8cb..0000000000000 --- a/.buildkite/scripts/steps/functional/security_serverless_rule_management_prebuilt_rules.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/steps/functional/common.sh -source .buildkite/scripts/steps/functional/common_cypress.sh - -export JOB=kibana-security-solution-chrome -export KIBANA_INSTALL_DIR=${KIBANA_BUILD_LOCATION} - -echo "--- Rule Management - Prebuilt Rules - Cypress Tests on Serverless" - -cd x-pack/test/security_solution_cypress - -set +e -yarn cypress:rule_management:prebuilt_rules:run:serverless; status=$?; yarn junit:merge || :; exit $status diff --git a/.buildkite/scripts/steps/functional/security_solution_rule_management.sh b/.buildkite/scripts/steps/functional/security_solution_rule_management.sh deleted file mode 100644 index 847cb42896cf1..0000000000000 --- a/.buildkite/scripts/steps/functional/security_solution_rule_management.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/steps/functional/common.sh -source .buildkite/scripts/steps/functional/common_cypress.sh - -export JOB=kibana-security-solution-chrome -export KIBANA_INSTALL_DIR=${KIBANA_BUILD_LOCATION} - -echo "--- Rule Management - Security Solution Cypress Tests" - -cd x-pack/test/security_solution_cypress - -set +e -yarn cypress:rule_management:run:ess; status=$?; yarn junit:merge || :; exit $status diff --git a/.buildkite/scripts/steps/functional/security_solution_rule_management_prebuilt_rules.sh b/.buildkite/scripts/steps/functional/security_solution_rule_management_prebuilt_rules.sh deleted file mode 100644 index d8b19ad3363b5..0000000000000 --- a/.buildkite/scripts/steps/functional/security_solution_rule_management_prebuilt_rules.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/steps/functional/common.sh -source .buildkite/scripts/steps/functional/common_cypress.sh - -export JOB=kibana-security-solution-chrome -export KIBANA_INSTALL_DIR=${KIBANA_BUILD_LOCATION} - -echo "--- Rule Management - Prebuilt Rules - Security Solution Cypress Tests" - -cd x-pack/test/security_solution_cypress - -set +e -yarn cypress:rule_management:prebuilt_rules:run:ess; status=$?; yarn junit:merge || :; exit $status diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7d075295240c9..89e152a2fe40f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1322,7 +1322,9 @@ x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout @elastic/ /x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring @elastic/security-detection-rule-management /x-pack/plugins/security_solution/common/detection_engine/rule_management @elastic/security-detection-rule-management +/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules @elastic/security-detection-rule-management /x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management @elastic/security-detection-rule-management +/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_details @elastic/security-detection-rule-management /x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules @elastic/security-detection-rule-management /x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/rule_management @elastic/security-detection-rule-management /x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/prebuilt_rules @elastic/security-detection-rule-management diff --git a/x-pack/test/security_solution_cypress/cypress/README.md b/x-pack/test/security_solution_cypress/cypress/README.md index 88786aed7ff56..8940d6c86e73e 100644 --- a/x-pack/test/security_solution_cypress/cypress/README.md +++ b/x-pack/test/security_solution_cypress/cypress/README.md @@ -62,25 +62,19 @@ Run the tests with the following yarn scripts from `x-pack/test/security_solutio | cypress | Runs the default Cypress command | | cypress:open:ess | Opens the Cypress UI with all tests in the `e2e` directory. This also runs a local kibana and ES instance. The kibana instance will reload when you make code changes. This is the recommended way to debug and develop tests. | | cypress:open:serverless | Opens the Cypress UI with all tests in the `e2e` directory. This also runs a mocked serverless environment. The kibana instance will reload when you make code changes. This is the recommended way to debug and develop tests. | -| cypress:run:ess | Runs all tests tagged as ESS placed in the `e2e` directory excluding `investigations`,`explore` and `detection_response/rule_management` directories in headless mode | +| cypress:run:ess | Runs all tests tagged as ESS placed in the `e2e` directory excluding `investigations` and `explore` directories in headless mode | | cypress:run:cases:ess | Runs all tests under `explore/cases` in the `e2e` directory related to the Cases area team in headless mode | | cypress:ess | Runs all ESS tests with the specified configuration in headless mode and produces a report using `cypress-multi-reporters` | -| cypress:rule_management:run:ess | Runs all tests tagged as ESS in the `e2e/detection_response/rule_management` excluding `e2e/detection_response/rule_management/prebuilt_rules` directory in headless mode | -| cypress:rule_management:prebuilt_rules:run:ess | Runs all tests tagged as ESS in the `e2e/detection_response/rule_management/prebuilt_rules` directory in headless mode | | cypress:run:respops:ess | Runs all tests related to the Response Ops area team, specifically tests in `detection_alerts`, `detection_rules`, and `exceptions` directories in headless mode | -| cypress:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e` directory excluding `investigations`, `explore` and `rule_management` directories in headless mode | -| cypress:rule_management:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e/detection_response/rule_management` excluding `e2e/detection_response/rule_management/prebuilt_rules` directory in headless mode | -| cypress:rule_management:prebuilt_rules:run:serverless | Runs all tests tagged as ESS in the `e2e/detection_response/rule_management/prebuilt_rules` directory in headless mode | -| cypress:investigations:run:ess | Runs all tests tagged as SERVERLESS in the `e2e/investigations` directory in headless mode | +| cypress:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e` directory excluding `investigations` and `explore` directories in headless mode | +| cypress:investigations:run:ess | Runs all tests tagged as ESS in the `e2e/investigations` directory in headless mode | | cypress:explore:run:ess | Runs all tests tagged as ESS in the `e2e/explore` directory in headless mode | | cypress:investigations:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e/investigations` directory in headless mode | | cypress:explore:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e/explore` directory in headless mode | | cypress:open:qa:serverless | Opens the Cypress UI with all tests in the `e2e` directory tagged as SERVERLESS. This also creates an MKI project in console.qa enviornment. The kibana instance will reload when you make code changes. This is the recommended way to debug tests in QA. Follow the readme in order to learn about the known limitations. | -| cypress:run:qa:serverless | Runs all tests tagged as SERVERLESS placed in the `e2e` directory excluding `investigations`, `explore` and `rule_management` directories in headless mode using the QA environment and real MKI projects.| +| cypress:run:qa:serverless | Runs all tests tagged as SERVERLESS placed in the `e2e` directory excluding `investigations` and `explore` directories in headless mode using the QA environment and real MKI projects.| | cypress:run:qa:serverless:explore | Runs all tests tagged as SERVERLESS in the `e2e/explore` directory in headless mode using the QA environment and real MKI prorjects. | | cypress:run:qa:serverless:investigations | Runs all tests tagged as SERVERLESS in the `e2e/investigations` directory in headless mode using the QA environment and reak MKI projects. | -| cypress:run:qa:serverless:rule_management | Runs all tests tagged as SERVERLESS in the `e2e/detection_response/rule_management` directory, excluding `e2e/detection_response/rule_management/prebuilt_rules` in headless mode using the QA environment and reak MKI projects. | -| cypress:run:qa:serverless:rule_management:prebuilt_rules | Runs all tests tagged as SERVERLESS in the `e2e/detection_response/rule_management/prebuilt_rules` directory in headless mode using the QA environment and reak MKI projects. | | junit:merge | Merges individual test reports into a single report and moves the report to the `junit` directory | Please note that all the headless mode commands do not open the Cypress UI and are typically used in CI/CD environments. The scripts that open the Cypress UI are useful for development and debugging. @@ -100,7 +94,7 @@ Below you can find the folder structure used on our Cypress tests. Cypress convention starting version 10 (previously known as integration). Contains the specs that are going to be executed. -### Area teams folders +### e2e/explore and e2e/investigations These directories contain tests which are run in their own Buildkite pipeline. @@ -109,8 +103,7 @@ If you belong to one of the teams listed in the table, please add new e2e specs | Directory | Area team | | -- | -- | | `e2e/explore` | Threat Hunting Explore | -| `e2e/investigations` | Threat Hunting Investigations | -| `e2e/detection_response/rule_management` | Detection Rule Management | +| `e2e/investigations | Threat Hunting Investigations | ### fixtures/ @@ -210,8 +203,6 @@ Run the tests with the following yarn scripts from `x-pack/test/security_solutio | cypress:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e` directory excluding `investigations` and `explore` directories in headless mode | | cypress:investigations:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e/investigations` directory in headless mode | | cypress:explore:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e/explore` directory in headless mode | -| cypress:rule_management:run:serverless | Runs all tests tagged as SERVERLESS in the `e2e/detection_response/rule_management` excluding `e2e/detection_response/rule_management/prebuilt_rules` directory in headless mode | -| cypress:rule_management:prebuilt_rules:run:serverless | Runs all tests tagged as ESS in the `e2e/detection_response/rule_management/prebuilt_rules` directory in headless mode | Please note that all the headless mode commands do not open the Cypress UI and are typically used in CI/CD environments. The scripts that open the Cypress UI are useful for development and debugging. @@ -257,8 +248,6 @@ Run the tests with the following yarn scripts from `x-pack/test/security_solutio | cypress:run:qa:serverless | Runs all tests tagged as SERVERLESS placed in the `e2e` directory excluding `investigations` and `explore` directories in headless mode using the QA environment and real MKI projects.| | cypress:run:qa:serverless:explore | Runs all tests tagged as SERVERLESS in the `e2e/explore` directory in headless mode using the QA environment and real MKI prorjects. | | cypress:run:qa:serverless:investigations | Runs all tests tagged as SERVERLESS in the `e2e/investigations` directory in headless mode using the QA environment and reak MKI projects. | -| cypress:run:qa:serverless:rule_management | Runs all tests tagged as SERVERLESS in the `e2e/detection_response/rule_management` directory, excluding `e2e/detection_response/rule_management/prebuilt_rules` in headless mode using the QA environment and reak MKI projects. | -| cypress:run:qa:serverless:rule_management:prebuilt_rules | Runs all tests tagged as SERVERLESS in the `e2e/detection_response/rule_management/prebuilt_rules` directory in headless mode using the QA environment and reak MKI projects. | Please note that all the headless mode commands do not open the Cypress UI and are typically used in CI/CD environments. The scripts that open the Cypress UI are useful for development and debugging. diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_update_authorization.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/install_update_authorization.cy.ts similarity index 94% rename from x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_update_authorization.cy.ts rename to x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/install_update_authorization.cy.ts index 29e650dd4de66..e0078dd54e7ea 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_update_authorization.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/install_update_authorization.cy.ts @@ -12,14 +12,14 @@ import { } from '@kbn/security-solution-plugin/common/constants'; import { ROLES } from '@kbn/security-solution-plugin/common/test'; -import { createRuleAssetSavedObject } from '../../../../helpers/rules'; +import { createRuleAssetSavedObject } from '../../../helpers/rules'; import { createAndInstallMockedPrebuiltRules, installPrebuiltRuleAssets, preventPrebuiltRulesPackageInstallation, -} from '../../../../tasks/api_calls/prebuilt_rules'; -import { visit } from '../../../../tasks/navigation'; -import { RULES_MANAGEMENT_URL } from '../../../../urls/rules_management'; +} from '../../../tasks/api_calls/prebuilt_rules'; +import { visit } from '../../../tasks/navigation'; +import { RULES_MANAGEMENT_URL } from '../../../urls/rules_management'; import { ADD_ELASTIC_RULES_BTN, getInstallSingleRuleButtonByRuleId, @@ -31,8 +31,8 @@ import { RULES_UPDATES_TAB, RULE_CHECKBOX, UPGRADE_ALL_RULES_BUTTON, -} from '../../../../screens/alerts_detection_rules'; -import { login } from '../../../../tasks/login'; +} from '../../../screens/alerts_detection_rules'; +import { login } from '../../../tasks/login'; // Rule to test update const RULE_1_ID = 'rule_1'; diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_update_error_handling.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/install_update_error_handling.cy.ts similarity index 94% rename from x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_update_error_handling.cy.ts rename to x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/install_update_error_handling.cy.ts index db84d92e4ddb6..7e288910ccb60 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_update_error_handling.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/install_update_error_handling.cy.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { createRuleAssetSavedObject } from '../../../../helpers/rules'; +import { createRuleAssetSavedObject } from '../../../helpers/rules'; import { getInstallSingleRuleButtonByRuleId, getUpgradeSingleRuleButtonByRuleId, @@ -14,14 +14,14 @@ import { SELECT_ALL_RULES_ON_PAGE_CHECKBOX, UPGRADE_ALL_RULES_BUTTON, UPGRADE_SELECTED_RULES_BUTTON, -} from '../../../../screens/alerts_detection_rules'; -import { selectRulesByName } from '../../../../tasks/alerts_detection_rules'; +} from '../../../screens/alerts_detection_rules'; +import { selectRulesByName } from '../../../tasks/alerts_detection_rules'; import { installPrebuiltRuleAssets, createAndInstallMockedPrebuiltRules, preventPrebuiltRulesPackageInstallation, -} from '../../../../tasks/api_calls/prebuilt_rules'; -import { login } from '../../../../tasks/login'; +} from '../../../tasks/api_calls/prebuilt_rules'; +import { login } from '../../../tasks/login'; import { clickAddElasticRulesButton, assertInstallationRequestIsComplete, @@ -33,8 +33,8 @@ import { assertRulesPresentInAddPrebuiltRulesTable, assertRuleUpgradeFailureToastShown, assertRulesPresentInRuleUpdatesTable, -} from '../../../../tasks/prebuilt_rules'; -import { visitRulesManagementTable } from '../../../../tasks/rules_management'; +} from '../../../tasks/prebuilt_rules'; +import { visitRulesManagementTable } from '../../../tasks/rules_management'; describe( 'Detection rules, Prebuilt Rules Installation and Update - Error handling', diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_via_fleet.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/install_via_fleet.cy.ts similarity index 90% rename from x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_via_fleet.cy.ts rename to x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/install_via_fleet.cy.ts index 762e79bb27003..6da3d58c0530d 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_via_fleet.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/install_via_fleet.cy.ts @@ -8,13 +8,13 @@ import type { BulkInstallPackageInfo } from '@kbn/fleet-plugin/common'; import type { Rule } from '@kbn/security-solution-plugin/public/detection_engine/rule_management/logic/types'; -import { resetRulesTableState } from '../../../../tasks/common'; -import { INSTALL_ALL_RULES_BUTTON, TOASTER } from '../../../../screens/alerts_detection_rules'; -import { getRuleAssets } from '../../../../tasks/api_calls/prebuilt_rules'; -import { login } from '../../../../tasks/login'; -import { clickAddElasticRulesButton } from '../../../../tasks/prebuilt_rules'; -import { visitRulesManagementTable } from '../../../../tasks/rules_management'; -import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common'; +import { resetRulesTableState } from '../../../tasks/common'; +import { INSTALL_ALL_RULES_BUTTON, TOASTER } from '../../../screens/alerts_detection_rules'; +import { getRuleAssets } from '../../../tasks/api_calls/prebuilt_rules'; +import { login } from '../../../tasks/login'; +import { clickAddElasticRulesButton } from '../../../tasks/prebuilt_rules'; +import { visitRulesManagementTable } from '../../../tasks/rules_management'; +import { deleteAlertsAndRules } from '../../../tasks/api_calls/common'; describe( 'Detection rules, Prebuilt Rules Installation and Update workflow', diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_workflow.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/install_workflow.cy.ts similarity index 85% rename from x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_workflow.cy.ts rename to x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/install_workflow.cy.ts index 523d0ec0ad4e0..ec4615bcf59e4 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_workflow.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/install_workflow.cy.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { resetRulesTableState } from '../../../../tasks/common'; -import { createRuleAssetSavedObject } from '../../../../helpers/rules'; +import { resetRulesTableState } from '../../../tasks/common'; +import { createRuleAssetSavedObject } from '../../../helpers/rules'; import { getInstallSingleRuleButtonByRuleId, GO_BACK_TO_RULES_TABLE_BUTTON, @@ -16,19 +16,19 @@ import { RULE_CHECKBOX, SELECT_ALL_RULES_ON_PAGE_CHECKBOX, TOASTER, -} from '../../../../screens/alerts_detection_rules'; -import { selectRulesByName } from '../../../../tasks/alerts_detection_rules'; -import { RULE_MANAGEMENT_PAGE_BREADCRUMB } from '../../../../screens/breadcrumbs'; -import { installPrebuiltRuleAssets } from '../../../../tasks/api_calls/prebuilt_rules'; -import { login } from '../../../../tasks/login'; +} from '../../../screens/alerts_detection_rules'; +import { selectRulesByName } from '../../../tasks/alerts_detection_rules'; +import { RULE_MANAGEMENT_PAGE_BREADCRUMB } from '../../../screens/breadcrumbs'; +import { installPrebuiltRuleAssets } from '../../../tasks/api_calls/prebuilt_rules'; +import { login } from '../../../tasks/login'; import { assertInstallationRequestIsComplete, assertRuleInstallationSuccessToastShown, assertRulesPresentInInstalledRulesTable, clickAddElasticRulesButton, -} from '../../../../tasks/prebuilt_rules'; -import { visitRulesManagementTable } from '../../../../tasks/rules_management'; -import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common'; +} from '../../../tasks/prebuilt_rules'; +import { visitRulesManagementTable } from '../../../tasks/rules_management'; +import { deleteAlertsAndRules } from '../../../tasks/api_calls/common'; describe( 'Detection rules, Prebuilt Rules Installation and Update workflow', diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/management.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/management.cy.ts similarity index 91% rename from x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/management.cy.ts rename to x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/management.cy.ts index 15e020b5e0663..f3101f513915f 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/management.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/management.cy.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { createRuleAssetSavedObject } from '../../../../helpers/rules'; +import { createRuleAssetSavedObject } from '../../../helpers/rules'; import { COLLAPSED_ACTION_BTN, ELASTIC_RULES_BTN, @@ -15,7 +15,7 @@ import { RULE_SWITCH, SELECT_ALL_RULES_ON_PAGE_CHECKBOX, INSTALL_ALL_RULES_BUTTON, -} from '../../../../screens/alerts_detection_rules'; +} from '../../../screens/alerts_detection_rules'; import { deleteFirstRule, disableAutoRefresh, @@ -24,24 +24,21 @@ import { selectRulesByName, waitForPrebuiltDetectionRulesToBeLoaded, waitForRuleToUpdate, -} from '../../../../tasks/alerts_detection_rules'; +} from '../../../tasks/alerts_detection_rules'; import { deleteSelectedRules, disableSelectedRules, enableSelectedRules, -} from '../../../../tasks/rules_bulk_actions'; +} from '../../../tasks/rules_bulk_actions'; import { createAndInstallMockedPrebuiltRules, getAvailablePrebuiltRulesCount, preventPrebuiltRulesPackageInstallation, -} from '../../../../tasks/api_calls/prebuilt_rules'; -import { - deleteAlertsAndRules, - deletePrebuiltRulesAssets, -} from '../../../../tasks/api_calls/common'; -import { login } from '../../../../tasks/login'; -import { visit } from '../../../../tasks/navigation'; -import { RULES_MANAGEMENT_URL } from '../../../../urls/rules_management'; +} from '../../../tasks/api_calls/prebuilt_rules'; +import { deleteAlertsAndRules, deletePrebuiltRulesAssets } from '../../../tasks/api_calls/common'; +import { login } from '../../../tasks/login'; +import { visit } from '../../../tasks/navigation'; +import { RULES_MANAGEMENT_URL } from '../../../urls/rules_management'; const rules = Array.from(Array(5)).map((_, i) => { return createRuleAssetSavedObject({ diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/notifications.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/notifications.cy.ts similarity index 92% rename from x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/notifications.cy.ts rename to x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/notifications.cy.ts index 4812efc740ae2..92bf9e7f1471c 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/notifications.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/notifications.cy.ts @@ -5,25 +5,22 @@ * 2.0. */ -import { createRuleAssetSavedObject } from '../../../../helpers/rules'; +import { createRuleAssetSavedObject } from '../../../helpers/rules'; import { ADD_ELASTIC_RULES_BTN, ADD_ELASTIC_RULES_EMPTY_PROMPT_BTN, RULES_UPDATES_TAB, -} from '../../../../screens/alerts_detection_rules'; -import { deleteFirstRule } from '../../../../tasks/alerts_detection_rules'; -import { - deleteAlertsAndRules, - deletePrebuiltRulesAssets, -} from '../../../../tasks/api_calls/common'; +} from '../../../screens/alerts_detection_rules'; +import { deleteFirstRule } from '../../../tasks/alerts_detection_rules'; +import { deleteAlertsAndRules, deletePrebuiltRulesAssets } from '../../../tasks/api_calls/common'; import { installAllPrebuiltRulesRequest, installPrebuiltRuleAssets, createAndInstallMockedPrebuiltRules, -} from '../../../../tasks/api_calls/prebuilt_rules'; -import { resetRulesTableState } from '../../../../tasks/common'; -import { login } from '../../../../tasks/login'; -import { visitRulesManagementTable } from '../../../../tasks/rules_management'; +} from '../../../tasks/api_calls/prebuilt_rules'; +import { resetRulesTableState } from '../../../tasks/common'; +import { login } from '../../../tasks/login'; +import { visitRulesManagementTable } from '../../../tasks/rules_management'; const RULE_1 = createRuleAssetSavedObject({ name: 'Test rule 1', diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/prebuilt_rules_preview.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/prebuilt_rules_preview.cy.ts similarity index 97% rename from x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/prebuilt_rules_preview.cy.ts rename to x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/prebuilt_rules_preview.cy.ts index 81f37b7760df2..6deeb6f5202c0 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/prebuilt_rules_preview.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/prebuilt_rules_preview.cy.ts @@ -12,22 +12,22 @@ import type { PrebuiltRuleAsset } from '@kbn/security-solution-plugin/server/lib import type { Threshold } from '@kbn/security-solution-plugin/common/api/detection_engine/model/rule_schema'; import { AlertSuppression } from '@kbn/security-solution-plugin/common/api/detection_engine/model/rule_schema'; -import { createRuleAssetSavedObject } from '../../../../helpers/rules'; +import { createRuleAssetSavedObject } from '../../../helpers/rules'; import { INSTALL_PREBUILT_RULE_BUTTON, INSTALL_PREBUILT_RULE_PREVIEW, UPDATE_PREBUILT_RULE_PREVIEW, UPDATE_PREBUILT_RULE_BUTTON, -} from '../../../../screens/alerts_detection_rules'; -import { RULE_MANAGEMENT_PAGE_BREADCRUMB } from '../../../../screens/breadcrumbs'; +} from '../../../screens/alerts_detection_rules'; +import { RULE_MANAGEMENT_PAGE_BREADCRUMB } from '../../../screens/breadcrumbs'; import { installPrebuiltRuleAssets, createAndInstallMockedPrebuiltRules, -} from '../../../../tasks/api_calls/prebuilt_rules'; -import { createSavedQuery, deleteSavedQueries } from '../../../../tasks/api_calls/saved_queries'; -import { fetchMachineLearningModules } from '../../../../tasks/api_calls/machine_learning'; -import { resetRulesTableState } from '../../../../tasks/common'; -import { login } from '../../../../tasks/login'; +} from '../../../tasks/api_calls/prebuilt_rules'; +import { createSavedQuery, deleteSavedQueries } from '../../../tasks/api_calls/saved_queries'; +import { fetchMachineLearningModules } from '../../../tasks/api_calls/machine_learning'; +import { resetRulesTableState } from '../../../tasks/common'; +import { login } from '../../../tasks/login'; import { assertRuleInstallationSuccessToastShown, assertRulesNotPresentInAddPrebuiltRulesTable, @@ -36,7 +36,7 @@ import { assertRuleUpgradeSuccessToastShown, clickAddElasticRulesButton, clickRuleUpdatesTab, -} from '../../../../tasks/prebuilt_rules'; +} from '../../../tasks/prebuilt_rules'; import { assertAlertSuppressionPropertiesShown, assertCommonPropertiesShown, @@ -55,13 +55,13 @@ import { closeRulePreview, openRuleInstallPreview, openRuleUpdatePreview, -} from '../../../../tasks/prebuilt_rules_preview'; -import { visitRulesManagementTable } from '../../../../tasks/rules_management'; +} from '../../../tasks/prebuilt_rules_preview'; +import { visitRulesManagementTable } from '../../../tasks/rules_management'; import { deleteAlertsAndRules, deleteDataView, postDataView, -} from '../../../../tasks/api_calls/common'; +} from '../../../tasks/api_calls/common'; const TEST_ENV_TAGS = ['@ess', '@serverless']; diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/update_workflow.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/update_workflow.ts similarity index 88% rename from x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/update_workflow.ts rename to x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/update_workflow.ts index d858280dd5294..edeb8ac98623b 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/update_workflow.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules/update_workflow.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { createRuleAssetSavedObject } from '../../../../helpers/rules'; +import { createRuleAssetSavedObject } from '../../../helpers/rules'; import { getUpgradeSingleRuleButtonByRuleId, NO_RULES_AVAILABLE_FOR_UPGRADE_MESSAGE, @@ -13,22 +13,22 @@ import { SELECT_ALL_RULES_ON_PAGE_CHECKBOX, UPGRADE_ALL_RULES_BUTTON, UPGRADE_SELECTED_RULES_BUTTON, -} from '../../../../screens/alerts_detection_rules'; -import { selectRulesByName } from '../../../../tasks/alerts_detection_rules'; -import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common'; +} from '../../../screens/alerts_detection_rules'; +import { selectRulesByName } from '../../../tasks/alerts_detection_rules'; +import { deleteAlertsAndRules } from '../../../tasks/api_calls/common'; import { installPrebuiltRuleAssets, createAndInstallMockedPrebuiltRules, -} from '../../../../tasks/api_calls/prebuilt_rules'; -import { resetRulesTableState } from '../../../../tasks/common'; -import { login } from '../../../../tasks/login'; +} from '../../../tasks/api_calls/prebuilt_rules'; +import { resetRulesTableState } from '../../../tasks/common'; +import { login } from '../../../tasks/login'; import { assertRulesNotPresentInRuleUpdatesTable, assertRuleUpgradeSuccessToastShown, assertUpgradeRequestIsComplete, clickRuleUpdatesTab, -} from '../../../../tasks/prebuilt_rules'; -import { visitRulesManagementTable } from '../../../../tasks/rules_management'; +} from '../../../tasks/prebuilt_rules'; +import { visitRulesManagementTable } from '../../../tasks/rules_management'; describe( 'Detection rules, Prebuilt Rules Installation and Update workflow', diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/common_flows.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_details/common_flows.cy.ts similarity index 88% rename from x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/common_flows.cy.ts rename to x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_details/common_flows.cy.ts index 0610786fc1b89..f5704122d9e33 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/common_flows.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_details/common_flows.cy.ts @@ -5,17 +5,17 @@ * 2.0. */ -import { deleteRuleFromDetailsPage } from '../../../../tasks/alerts_detection_rules'; +import { deleteRuleFromDetailsPage } from '../../../tasks/alerts_detection_rules'; import { CUSTOM_RULES_BTN, RULES_MANAGEMENT_TABLE, RULES_ROW, -} from '../../../../screens/alerts_detection_rules'; -import { createRule } from '../../../../tasks/api_calls/rules'; -import { getDetails } from '../../../../tasks/rule_details'; -import { ruleFields } from '../../../../data/detection_engine'; -import { getTimeline } from '../../../../objects/timeline'; -import { getExistingRule, getNewRule } from '../../../../objects/rule'; +} from '../../../screens/alerts_detection_rules'; +import { createRule } from '../../../tasks/api_calls/rules'; +import { getDetails } from '../../../tasks/rule_details'; +import { ruleFields } from '../../../data/detection_engine'; +import { getTimeline } from '../../../objects/timeline'; +import { getExistingRule, getNewRule } from '../../../objects/rule'; import { ABOUT_DETAILS, @@ -42,13 +42,13 @@ import { THREAT_TACTIC, THREAT_TECHNIQUE, TIMELINE_TEMPLATE_DETAILS, -} from '../../../../screens/rule_details'; +} from '../../../screens/rule_details'; -import { createTimeline } from '../../../../tasks/api_calls/timelines'; -import { deleteAlertsAndRules, deleteConnectors } from '../../../../tasks/api_calls/common'; -import { login } from '../../../../tasks/login'; -import { visit } from '../../../../tasks/navigation'; -import { ruleDetailsUrl } from '../../../../urls/rule_details'; +import { createTimeline } from '../../../tasks/api_calls/timelines'; +import { deleteAlertsAndRules, deleteConnectors } from '../../../tasks/api_calls/common'; +import { login } from '../../../tasks/login'; +import { visit } from '../../../tasks/navigation'; +import { ruleDetailsUrl } from '../../../urls/rule_details'; // This test is meant to test all common aspects of the rule details page that should function // the same regardless of rule type. For any rule type specific functionalities, please include diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/esql_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_details/esql_rule.cy.ts similarity index 69% rename from x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/esql_rule.cy.ts rename to x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_details/esql_rule.cy.ts index c59b7db55c743..7d1419e911e33 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_details/esql_rule.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_details/esql_rule.cy.ts @@ -5,24 +5,24 @@ * 2.0. */ -import { getEsqlRule } from '../../../../objects/rule'; +import { getEsqlRule } from '../../../objects/rule'; import { ESQL_QUERY_DETAILS, DEFINITION_DETAILS, RULE_NAME_HEADER, RULE_TYPE_DETAILS, -} from '../../../../screens/rule_details'; +} from '../../../screens/rule_details'; -import { createRule } from '../../../../tasks/api_calls/rules'; +import { createRule } from '../../../tasks/api_calls/rules'; -import { getDetails } from '../../../../tasks/rule_details'; -import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common'; +import { getDetails } from '../../../tasks/rule_details'; +import { deleteAlertsAndRules } from '../../../tasks/api_calls/common'; -import { login } from '../../../../tasks/login'; -import { visit } from '../../../../tasks/navigation'; +import { login } from '../../../tasks/login'; +import { visit } from '../../../tasks/navigation'; -import { ruleDetailsUrl } from '../../../../urls/rule_details'; +import { ruleDetailsUrl } from '../../../urls/rule_details'; describe('Detection ES|QL rules, details view', { tags: ['@ess'] }, () => { const rule = getEsqlRule(); diff --git a/x-pack/test/security_solution_cypress/package.json b/x-pack/test/security_solution_cypress/package.json index e1f552fdba9de..e43f32a447575 100644 --- a/x-pack/test/security_solution_cypress/package.json +++ b/x-pack/test/security_solution_cypress/package.json @@ -7,11 +7,9 @@ "scripts": { "cypress": "NODE_OPTIONS=--openssl-legacy-provider ../../../node_modules/.bin/cypress", "cypress:open:ess": "TZ=UTC NODE_OPTIONS=--openssl-legacy-provider node ../../plugins/security_solution/scripts/start_cypress_parallel open --spec './cypress/e2e/**/*.cy.ts' --config-file ../../test/security_solution_cypress/cypress/cypress.config.ts --ftr-config-file ../../test/security_solution_cypress/cli_config", - "cypress:run:ess": "yarn cypress:ess --spec './cypress/e2e/!(investigations|explore|detection_response/rule_management)/**/*.cy.ts'", + "cypress:run:ess": "yarn cypress:ess --spec './cypress/e2e/!(investigations|explore)/**/*.cy.ts'", "cypress:run:cases:ess": "yarn cypress:ess --spec './cypress/e2e/explore/cases/*.cy.ts'", "cypress:ess": "TZ=UTC NODE_OPTIONS=--openssl-legacy-provider node ../../plugins/security_solution/scripts/start_cypress_parallel run --config-file ../../test/security_solution_cypress/cypress/cypress_ci.config.ts --ftr-config-file ../../test/security_solution_cypress/cli_config", - "cypress:rule_management:run:ess":"yarn cypress:ess --spec './cypress/e2e/detection_response/rule_management/!(prebuilt_rules)/**/*.cy.ts'", - "cypress:rule_management:prebuilt_rules:run:ess": "yarn cypress:ess --spec './cypress/e2e/detection_response/rule_management/prebuilt_rules/**/*.cy.ts'", "cypress:run:respops:ess": "yarn cypress:ess --spec './cypress/e2e/(detection_response|exceptions)/**/*.cy.ts'", "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'", @@ -23,20 +21,16 @@ "cypress:cloud:serverless": "TZ=UTC NODE_OPTIONS=--openssl-legacy-provider NODE_TLS_REJECT_UNAUTHORIZED=0 ../../../node_modules/.bin/cypress", "cypress:open:cloud:serverless": "yarn cypress:cloud:serverless open --config-file ./cypress/cypress_serverless.config.ts --env CLOUD_SERVERLESS=true", "cypress:open:serverless": "yarn cypress:serverless open --config-file ../../test/security_solution_cypress/cypress/cypress_serverless.config.ts --spec './cypress/e2e/**/*.cy.ts'", - "cypress:run:serverless": "yarn cypress:serverless --spec './cypress/e2e/!(investigations|explore|detection_response/rule_management)/**/*.cy.ts'", + "cypress:run:serverless": "yarn cypress:serverless --spec './cypress/e2e/!(investigations|explore)/**/*.cy.ts'", "cypress:run:cloud:serverless": "yarn cypress:cloud:serverless run --config-file ./cypress/cypress_ci_serverless.config.ts --env CLOUD_SERVERLESS=true", - "cypress:rule_management:run:serverless": "yarn cypress:serverless --spec './cypress/e2e/detection_response/rule_management/!(prebuilt_rules)/**/*.cy.ts'", - "cypress:rule_management:prebuilt_rules:run:serverless": "yarn cypress:serverless --spec './cypress/e2e/detection_response/rule_management/prebuilt_rules/**/*.cy.ts'", "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: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", - "cypress:run:qa:serverless": "yarn cypress:qa:serverless --spec './cypress/e2e/!(investigations|explore|detection_response/rule_management)/**/*.cy.ts'", + "cypress:run:qa:serverless": "yarn cypress:qa:serverless --spec './cypress/e2e/!(investigations|explore)/**/*.cy.ts'", "cypress:run:qa:serverless:investigations": "yarn cypress:qa:serverless --spec './cypress/e2e/investigations/**/*.cy.ts'", - "cypress:run:qa:serverless:explore": "yarn cypress:qa:serverless --spec './cypress/e2e/explore/**/*.cy.ts'", - "cypress:run:qa:serverless:rule_management": "yarn cypress:qa:serverless --spec './cypress/e2e/detection_response/rule_management/!(prebuilt_rules)/**/*.cy.ts'", - "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:explore": "yarn cypress:qa:serverless --spec './cypress/e2e/explore/**/*.cy.ts'" } } \ No newline at end of file From a6582337e1c8b7f8f335694b92f791cb274dc519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20=C3=81brah=C3=A1m?= Date: Tue, 28 Nov 2023 15:11:24 +0100 Subject: [PATCH 14/14] [Defend Workflows][8.12 port] Unblock fleet setup when cannot decrypt uninstall tokens (#172058) ## Summary This PR is the `8.12` port of: - #171998 The original PR was opened to `8.11` to make it faster to include it in `8.12.2`. Now this PR is meant to port the changes to `main`, so: - we can build upon it, - and can easily backport any further changes to `8.11.x` > [!Important] > The changes cannot be tested on `main` because they are hidden by other behaviours (namely the retry logic for reading Message SIgning key) that weren't part of `8.11`. Those behaviours will be also adapted in follow up PRs. --- x-pack/plugins/fleet/server/mocks/index.ts | 2 + .../server/services/agent_policy.test.ts | 43 ++++++++-- .../fleet/server/services/agent_policy.ts | 15 ++++ .../uninstall_token_service/index.test.ts | 75 +++++++++++++++++ .../security/uninstall_token_service/index.ts | 80 +++++++++++-------- x-pack/plugins/fleet/server/services/setup.ts | 16 +++- 6 files changed, 189 insertions(+), 42 deletions(-) diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index 2716fd82b6811..ec8ada164623d 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -201,5 +201,7 @@ export function createUninstallTokenServiceMock(): UninstallTokenServiceInterfac generateTokensForPolicyIds: jest.fn(), generateTokensForAllPolicies: jest.fn(), encryptTokens: jest.fn(), + checkTokenValidityForAllPolicies: jest.fn(), + checkTokenValidityForPolicy: jest.fn(), }; } diff --git a/x-pack/plugins/fleet/server/services/agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policy.test.ts index 710ac46b94592..b6950ba672817 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.test.ts @@ -32,6 +32,7 @@ import { getFullAgentPolicy } from './agent_policies'; import * as outputsHelpers from './agent_policies/outputs_helpers'; import { auditLoggingService } from './audit_logging'; import { licenseService } from './license'; +import type { UninstallTokenServiceInterface } from './security/uninstall_token_service'; function getSavedObjectMock(agentPolicyAttributes: any) { const mock = savedObjectsClientMock.create(); @@ -182,13 +183,13 @@ describe('agent policy', () => { }); }); - it('should throw FleetUnauthorizedError if is_protected=true with insufficient license', () => { + it('should throw FleetUnauthorizedError if is_protected=true with insufficient license', async () => { jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(false); const soClient = getAgentPolicyCreateMock(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - expect( + await expect( agentPolicyService.create(soClient, esClient, { name: 'test', namespace: 'default', @@ -199,13 +200,13 @@ describe('agent policy', () => { ); }); - it('should not throw FleetUnauthorizedError if is_protected=false with insufficient license', () => { + it('should not throw FleetUnauthorizedError if is_protected=false with insufficient license', async () => { jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(false); const soClient = getAgentPolicyCreateMock(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - expect( + await expect( agentPolicyService.create(soClient, esClient, { name: 'test', namespace: 'default', @@ -619,7 +620,7 @@ describe('agent policy', () => { }); }); - it('should throw FleetUnauthorizedError if is_protected=true with insufficient license', () => { + it('should throw FleetUnauthorizedError if is_protected=true with insufficient license', async () => { jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(false); const soClient = getAgentPolicyCreateMock(); @@ -632,7 +633,7 @@ describe('agent policy', () => { references: [], }); - expect( + await expect( agentPolicyService.update(soClient, esClient, 'test-id', { name: 'test', namespace: 'default', @@ -643,7 +644,7 @@ describe('agent policy', () => { ); }); - it('should not throw FleetUnauthorizedError if is_protected=false with insufficient license', () => { + it('should not throw FleetUnauthorizedError if is_protected=false with insufficient license', async () => { jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(false); const soClient = getAgentPolicyCreateMock(); @@ -656,7 +657,7 @@ describe('agent policy', () => { references: [], }); - expect( + await expect( agentPolicyService.update(soClient, esClient, 'test-id', { name: 'test', namespace: 'default', @@ -665,6 +666,32 @@ describe('agent policy', () => { new FleetUnauthorizedError('Tamper protection requires Platinum license') ); }); + + it('should throw Error if is_protected=true with invalid uninstall token', async () => { + jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true); + + mockedAppContextService.getUninstallTokenService.mockReturnValueOnce({ + checkTokenValidityForPolicy: jest.fn().mockRejectedValueOnce(new Error('reason')), + } as unknown as UninstallTokenServiceInterface); + + const soClient = getAgentPolicyCreateMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + soClient.get.mockResolvedValue({ + attributes: {}, + id: 'test-id', + type: 'mocked', + references: [], + }); + + await expect( + agentPolicyService.update(soClient, esClient, 'test-id', { + name: 'test', + namespace: 'default', + is_protected: true, + }) + ).rejects.toThrowError(new Error('Cannot enable Agent Tamper Protection: reason')); + }); }); describe('deployPolicy', () => { diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 8673bd6ad91a9..5e8c897d5611a 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -512,6 +512,7 @@ class AgentPolicyService { } this.checkTamperProtectionLicense(agentPolicy); + await this.checkForValidUninstallToken(agentPolicy, id); const logger = appContextService.getLogger(); @@ -1212,6 +1213,20 @@ class AgentPolicyService { throw new FleetUnauthorizedError('Tamper protection requires Platinum license'); } } + private async checkForValidUninstallToken( + agentPolicy: { is_protected?: boolean }, + policyId: string + ): Promise { + if (agentPolicy?.is_protected) { + const uninstallTokenService = appContextService.getUninstallTokenService(); + + try { + await uninstallTokenService?.checkTokenValidityForPolicy(policyId); + } catch (e) { + throw new Error(`Cannot enable Agent Tamper Protection: ${e.message}`); + } + } + } } export const agentPolicyService = new AgentPolicyService(); diff --git a/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.test.ts b/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.test.ts index 4cf657b7255c5..4b3be1de81632 100644 --- a/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.test.ts +++ b/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.test.ts @@ -499,5 +499,80 @@ describe('UninstallTokenService', () => { }); }); }); + + describe('check validity of tokens', () => { + const okaySO = getDefaultSO(canEncrypt); + + const errorWithDecryptionSO2 = { + ...getDefaultSO2(canEncrypt), + error: new Error('error reason'), + }; + const missingTokenSO2 = { + ...getDefaultSO2(canEncrypt), + attributes: { + ...getDefaultSO2(canEncrypt).attributes, + token: undefined, + token_plain: undefined, + }, + }; + + describe('checkTokenValidityForAllPolicies', () => { + it('resolves if all of the tokens are available', async () => { + mockCreatePointInTimeFinderAsInternalUser(); + + await expect( + uninstallTokenService.checkTokenValidityForAllPolicies() + ).resolves.not.toThrowError(); + }); + + it('rejects if any of the tokens is missing', async () => { + mockCreatePointInTimeFinderAsInternalUser([okaySO, missingTokenSO2]); + + await expect( + uninstallTokenService.checkTokenValidityForAllPolicies() + ).rejects.toThrowError( + 'Invalid uninstall token: Saved object is missing the `token` attribute.' + ); + }); + + it('rejects if token decryption gives error', async () => { + mockCreatePointInTimeFinderAsInternalUser([okaySO, errorWithDecryptionSO2]); + + await expect( + uninstallTokenService.checkTokenValidityForAllPolicies() + ).rejects.toThrowError('Error when reading Uninstall Token: error reason'); + }); + }); + + describe('checkTokenValidityForPolicy', () => { + it('resolves if token is available', async () => { + mockCreatePointInTimeFinderAsInternalUser(); + + await expect( + uninstallTokenService.checkTokenValidityForPolicy(okaySO.attributes.policy_id) + ).resolves.not.toThrowError(); + }); + + it('rejects if token is missing', async () => { + mockCreatePointInTimeFinderAsInternalUser([okaySO, missingTokenSO2]); + + await expect( + uninstallTokenService.checkTokenValidityForPolicy(missingTokenSO2.attributes.policy_id) + ).rejects.toThrowError( + 'Invalid uninstall token: Saved object is missing the `token` attribute.' + ); + }); + + it('rejects if token decryption gives error', async () => { + mockCreatePointInTimeFinderAsInternalUser([okaySO, errorWithDecryptionSO2]); + + await expect( + uninstallTokenService.checkTokenValidityForPolicy( + errorWithDecryptionSO2.attributes.policy_id + ) + ).rejects.toThrowError('Error when reading Uninstall Token: error reason'); + }); + }); + }); }); }); diff --git a/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.ts b/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.ts index 8309910be6f53..9e03e7869c584 100644 --- a/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.ts +++ b/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.ts @@ -109,7 +109,7 @@ export interface UninstallTokenServiceInterface { * @param force generate a new token even if one already exists * @returns hashedToken */ - generateTokenForPolicyId(policyId: string, force?: boolean): Promise; + generateTokenForPolicyId(policyId: string, force?: boolean): Promise; /** * Generate uninstall tokens for given policy ids @@ -119,7 +119,7 @@ export interface UninstallTokenServiceInterface { * @param force generate a new token even if one already exists * @returns Record */ - generateTokensForPolicyIds(policyIds: string[], force?: boolean): Promise>; + generateTokensForPolicyIds(policyIds: string[], force?: boolean): Promise; /** * Generate uninstall tokens all policies @@ -128,12 +128,26 @@ export interface UninstallTokenServiceInterface { * @param force generate a new token even if one already exists * @returns Record */ - generateTokensForAllPolicies(force?: boolean): Promise>; + generateTokensForAllPolicies(force?: boolean): Promise; /** * If encryption is available, checks for any plain text uninstall tokens and encrypts them */ encryptTokens(): Promise; + + /** + * Check whether the selected policy has a valid uninstall token. Rejects returning promise if not. + * + * @param policyId policy Id to check + */ + checkTokenValidityForPolicy(policyId: string): Promise; + + /** + * Check whether all policies have a valid uninstall token. Rejects returning promise if not. + * + * @param policyId policy Id to check + */ + checkTokenValidityForAllPolicies(): Promise; } export class UninstallTokenService implements UninstallTokenServiceInterface { @@ -210,7 +224,11 @@ export class UninstallTokenService implements UninstallTokenServiceInterface { tokensFinder.close(); const uninstallTokens: UninstallToken[] = tokenObject.map( - ({ id: _id, attributes, created_at: createdAt }) => { + ({ id: _id, attributes, created_at: createdAt, error }) => { + if (error) { + throw new UninstallTokenError(`Error when reading Uninstall Token: ${error.message}`); + } + this.assertPolicyId(attributes); this.assertToken(attributes); this.assertCreatedAt(createdAt); @@ -304,32 +322,30 @@ export class UninstallTokenService implements UninstallTokenServiceInterface { return this.getHashedTokensForPolicyIds(policyIds); } - public async generateTokenForPolicyId(policyId: string, force: boolean = false): Promise { - return (await this.generateTokensForPolicyIds([policyId], force))[policyId]; + public generateTokenForPolicyId(policyId: string, force: boolean = false): Promise { + return this.generateTokensForPolicyIds([policyId], force); } public async generateTokensForPolicyIds( policyIds: string[], force: boolean = false - ): Promise> { + ): Promise { const { agentTamperProtectionEnabled } = appContextService.getExperimentalFeatures(); if (!agentTamperProtectionEnabled || !policyIds.length) { - return {}; + return; } - const existingTokens = force - ? {} - : (await this.getDecryptedTokensForPolicyIds(policyIds)).reduce( - (acc, { policy_id: policyId, token }) => { - acc[policyId] = token; - return acc; - }, - {} as Record - ); + const existingTokens = new Set(); + + if (!force) { + (await this.getTokenObjectsByIncludeFilter(policyIds)).forEach((tokenObject) => { + existingTokens.add(tokenObject._source[UNINSTALL_TOKENS_SAVED_OBJECT_TYPE].policy_id); + }); + } const missingTokenPolicyIds = force ? policyIds - : policyIds.filter((policyId) => !existingTokens[policyId]); + : policyIds.filter((policyId) => !existingTokens.has(policyId)); const newTokensMap = missingTokenPolicyIds.reduce((acc, policyId) => { const token = this.generateToken(); @@ -338,7 +354,6 @@ export class UninstallTokenService implements UninstallTokenServiceInterface { [policyId]: token, }; }, {} as Record); - await this.persistTokens(missingTokenPolicyIds, newTokensMap); if (force) { const config = appContextService.getConfig(); @@ -349,21 +364,9 @@ export class UninstallTokenService implements UninstallTokenServiceInterface { await agentPolicyService.deployPolicies(this.soClient, policyIdsBatch) ); } - - const tokensMap = { - ...existingTokens, - ...newTokensMap, - }; - - return Object.entries(tokensMap).reduce((acc, [policyId, token]) => { - acc[policyId] = this.hashToken(token); - return acc; - }, {} as Record); } - public async generateTokensForAllPolicies( - force: boolean = false - ): Promise> { + public async generateTokensForAllPolicies(force: boolean = false): Promise { const policyIds = await this.getAllPolicyIds(); return this.generateTokensForPolicyIds(policyIds, force); } @@ -486,6 +489,15 @@ export class UninstallTokenService implements UninstallTokenServiceInterface { return this._soClient; } + public async checkTokenValidityForPolicy(policyId: string): Promise { + await this.getDecryptedTokensForPolicyIds([policyId]); + } + + public async checkTokenValidityForAllPolicies(): Promise { + const policyIds = await this.getAllPolicyIds(); + await this.getDecryptedTokensForPolicyIds(policyIds); + } + private get isEncryptionAvailable(): boolean { return appContextService.getEncryptedSavedObjectsSetup()?.canEncrypt ?? false; } @@ -498,7 +510,9 @@ export class UninstallTokenService implements UninstallTokenServiceInterface { private assertToken(attributes: UninstallTokenSOAttributes | undefined) { if (!attributes?.token && !attributes?.token_plain) { - throw new UninstallTokenError('Uninstall Token is missing the token.'); + throw new UninstallTokenError( + 'Invalid uninstall token: Saved object is missing the `token` attribute.' + ); } } diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index 178499011bc61..60ce6460d0ac2 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -6,6 +6,7 @@ */ import fs from 'fs/promises'; + import apm from 'elastic-apm-node'; import { compact } from 'lodash'; @@ -13,6 +14,8 @@ import pMap from 'p-map'; import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; +import type { UninstallTokenError } from '../../common/errors'; + import { AUTO_UPDATE_PACKAGES } from '../../common/constants'; import type { PreconfigurationError } from '../../common/constants'; import type { DefaultPackagesInstallationError } from '../../common/types'; @@ -54,7 +57,10 @@ import { cleanUpOldFileIndices } from './setup/clean_old_fleet_indices'; export interface SetupStatus { isInitialized: boolean; nonFatalErrors: Array< - PreconfigurationError | DefaultPackagesInstallationError | UpgradeManagedPackagePoliciesResult + | PreconfigurationError + | DefaultPackagesInstallationError + | UpgradeManagedPackagePoliciesResult + | { error: UninstallTokenError } >; } @@ -196,9 +202,17 @@ async function createSetupSideEffects( logger.debug('Checking for and encrypting plain text uninstall tokens'); await appContextService.getUninstallTokenService()?.encryptTokens(); } + + logger.debug('Checking validity of Uninstall Tokens'); + try { + await appContextService.getUninstallTokenService()?.checkTokenValidityForAllPolicies(); + } catch (error) { + nonFatalErrors.push({ error }); + } stepSpan?.end(); stepSpan = apm.startSpan('Upgrade agent policy schema', 'preconfiguration'); + logger.debug('Upgrade Agent policy schema version'); await upgradeAgentPolicySchemaVersion(soClient); stepSpan?.end();