From a45ecf09cccd3d739ffd0105b2de524868556313 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Thu, 6 Feb 2025 18:41:21 +0100 Subject: [PATCH] [Security Solution] SIEM Migrations RBAC (#207087) ## Summary Implements the access controls for SIEM rule migrations. ## API changes - All API routes have been secured with "SIEM Migration" feature checks - Start migration API route now checks if the user has privileges to use the connector ID received ## UI changes ### Onboarding SIEM migrations - AI Connector selection - Actions & Connectors: Read -> This privilege allows reading and selecting a connector Otherwise, we show a callout with the missing privileges: ![connector read missing](https://github.com/user-attachments/assets/2eb474df-78f0-488c-803b-7c874123b62a) - Create a migration - Security All -> Main Security read & write access - Siem Migrations All -> new feature under the Security catalog - Actions & Connectors: Read -> This privilege allows connector execution for LLM calls Otherwise, we show a callout with the missing privileges: ![onboarding start card callout](https://github.com/user-attachments/assets/19975efd-d684-47d8-b4c0-0352b7c319b4) ### Rule Translations page - Minimum privileges to make the page accessible (read access): - Security Read -> Main Security read access - Siem Migrations All -> new feature under the Security catalog Otherwise, we hide the link in the navigation and display the generic empty state if accessed: ![rules minimum privileges missing](https://github.com/user-attachments/assets/9dd88c72-e669-4fde-8397-e76d3d5069f9) - To successfully install rules the following privileges are also required (write access): - Security All -> Main Security read & write access - Index privileges for `.alerts*` pattern: _read, write, view_index_metadata, manage_ - Index privileges for `lookup_*` pattern: _read_ Otherwise, we show a callout at the top of the page, this callout is consistent with the one displayed on the Detection Rules page (`/app/security/rules`) ![alerts privileges missing](https://github.com/user-attachments/assets/105e53d7-9591-457f-983a-7fe4f9f33068) - To retry rule translations (upload missing macros/lookups or retry errors) - Actions & Connectors: Read -> This privilege allows connector execution for LLM calls Otherwise, when attempted, we show a toast with the missing privilege. ![](https://github.com/user-attachments/assets/f6090bb5-e6f8-4be7-bb9b-c4192155bdf8) ## Other changes - Technical preview label ![technical preview](https://github.com/user-attachments/assets/244724e2-9756-4c6d-805f-3459367f7975) - No connector selected toast https://github.com/user-attachments/assets/e4900129-ae9c-413f-9a41-f7dca452e71d ## Fixes - [Fixed] Not possible to select a connector when no connector is selected: ![bug connectors](https://github.com/user-attachments/assets/2f5a831e-2172-4e77-9997-2447b4ee866f) --------- Co-authored-by: Elastic Machine Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> (cherry picked from commit a990be66dffbe89b271722630fd78b544b6ae903) --- .../security/packages/features/actions.ts | 7 + .../security/packages/features/config.ts | 1 + .../security/packages/features/constants.ts | 7 + .../packages/features/product_features.ts | 1 + .../security/packages/features/src/actions.ts | 14 ++ .../features/src/assistant/kibana_features.ts | 2 +- .../src/attack_discovery/kibana_features.ts | 2 +- .../src/cases/v1_features/kibana_features.ts | 2 +- .../src/cases/v2_features/kibana_features.ts | 2 +- .../packages/features/src/constants.ts | 1 + .../features/src/product_features_keys.ts | 8 + .../features/src/siem_migrations/index.ts | 15 ++ .../src/siem_migrations/kibana_features.ts | 50 +++++ .../siem_migrations/product_feature_config.ts | 34 ++++ .../security/packages/features/src/types.ts | 16 ++ .../src/solution_side_nav_panel.test.tsx | 4 - .../side_nav/src/solution_side_nav_panel.tsx | 4 +- .../common/api/quickstart_client.gen.ts | 20 +- .../common/siem_migrations/constants.ts | 5 + .../model/api/rules/rule_migration.gen.ts | 27 ++- .../api/rules/rule_migration.schema.yaml | 100 +++++++--- .../lib/capabilities/capabilities_checker.ts | 19 ++ .../lib/capabilities/has_capabilities.ts | 3 +- .../public/common/lib/capabilities/index.ts | 1 + .../components/hooks/use_stored_state.ts | 5 +- .../onboarding/components/onboarding.tsx | 20 +- .../cards/assistant/assistant_card.tsx | 6 +- .../common/connectors/connector_cards.tsx | 29 +-- .../connectors/connector_selector_panel.tsx | 87 +++++++-- .../connector_selector_with_icon.tsx | 118 ------------ .../common/connectors/connector_setup.tsx | 161 ++++++++--------- .../connectors/hooks/use_load_action_types.ts | 17 -- .../common/connectors/missing_privileges.tsx | 55 ++---- .../cards/common/connectors/translations.ts | 27 +-- .../cards/common/missing_privileges/index.ts | 12 ++ .../missing_privileges/missing_privileges.tsx | 70 +++++++ .../common/missing_privileges/translations.ts | 29 +++ .../ai_connector/ai_connector_card.tsx | 10 +- .../ai_connector/connectors_check_complete.ts | 26 +-- .../siem_migrations/start_migration/index.ts | 5 +- .../start_migration/start_migration_card.tsx | 29 ++- .../start_migration_check_complete.ts | 27 ++- .../siem_migrations/start_migration/types.ts | 10 + .../components/onboarding_route.tsx | 38 ---- .../components/onboarding_router.tsx | 66 +++++++ .../public/onboarding/config.ts | 2 + .../public/siem_migrations/links.ts | 8 +- .../public/siem_migrations/rules/api/index.ts | 28 ++- .../logic/use_get_migration_privileges.ts | 41 +++++ .../rules/logic/use_update_migration_rule.ts | 6 +- .../siem_migrations/rules/pages/index.tsx | 43 ++--- .../pages/missing_privileges_callout.tsx | 72 ++++++++ .../rules/service/capabilities.ts | 75 ++++++++ .../service/hooks/use_start_migration.ts | 6 +- .../missing_capabilities_notification.tsx | 52 ++++++ .../no_connector_notification.tsx | 67 +++++++ .../success_notification.tsx | 2 +- .../rules/service/rule_migrations_service.ts | 38 +++- .../rules/service/translations.ts | 5 - .../lib/product_features_service/mocks.ts | 20 ++ .../product_features_service.test.ts | 30 ++- .../product_features_service.ts | 38 ++-- .../lib/product_features_service/types.ts | 22 --- .../lib/siem_migrations/rules/api/create.ts | 31 ++-- .../lib/siem_migrations/rules/api/get.ts | 66 +++---- .../rules/api/get_integrations.ts | 3 +- .../rules/api/get_prebuilt_rules.ts | 3 +- .../lib/siem_migrations/rules/api/index.ts | 3 + .../lib/siem_migrations/rules/api/install.ts | 30 ++- .../api/privileges/get_missing_privileges.ts | 78 ++++++++ .../rules/api/resources/get.ts | 46 +---- .../rules/api/resources/missing.ts | 3 +- .../rules/api/resources/upsert.ts | 49 +---- .../lib/siem_migrations/rules/api/start.ts | 36 ++-- .../lib/siem_migrations/rules/api/stats.ts | 3 +- .../siem_migrations/rules/api/stats_all.ts | 3 +- .../lib/siem_migrations/rules/api/stop.ts | 28 +-- .../rules/api/translation_stats.ts | 3 +- .../lib/siem_migrations/rules/api/update.ts | 48 +++-- .../siem_migrations/rules/api/util/audit.ts | 171 +++++++++++++----- .../siem_migrations/rules/api/util/authz.ts | 12 ++ .../rule_migrations_data_lookups_client.ts | 3 +- .../server/product_features/index.ts | 10 +- ...siem_migrations_product_features_config.ts | 41 +++++ .../common/pli/pli_config.ts | 1 + .../server/product_features/index.ts | 6 +- ...siem_migrations_product_features_config.ts | 40 ++++ .../apis/features/features/features.ts | 2 + .../apis/security/privileges.ts | 1 + .../apis/security/privileges_basic.ts | 2 + .../services/security_solution_api.gen.ts | 23 ++- .../common/suites/create.agnostic.ts | 1 + .../common/suites/get.agnostic.ts | 1 + .../common/suites/get_all.agnostic.ts | 1 + .../spaces_only/telemetry/telemetry.ts | 1 + .../security_and_spaces/tests/nav_links.ts | 1 + 96 files changed, 1690 insertions(+), 807 deletions(-) create mode 100644 x-pack/solutions/security/packages/features/actions.ts create mode 100644 x-pack/solutions/security/packages/features/constants.ts create mode 100644 x-pack/solutions/security/packages/features/src/actions.ts create mode 100644 x-pack/solutions/security/packages/features/src/siem_migrations/index.ts create mode 100644 x-pack/solutions/security/packages/features/src/siem_migrations/kibana_features.ts create mode 100644 x-pack/solutions/security/packages/features/src/siem_migrations/product_feature_config.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/common/lib/capabilities/capabilities_checker.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_selector_with_icon.tsx delete mode 100644 x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/hooks/use_load_action_types.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/missing_privileges/index.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/missing_privileges/missing_privileges.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/missing_privileges/translations.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/types.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_route.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_router.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_privileges.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/pages/missing_privileges_callout.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/capabilities.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/notifications/missing_capabilities_notification.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/notifications/no_connector_notification.tsx rename x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/{ => notifications}/success_notification.tsx (97%) delete mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/types.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/privileges/get_missing_privileges.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/authz.ts create mode 100644 x-pack/solutions/security/plugins/security_solution_ess/server/product_features/siem_migrations_product_features_config.ts create mode 100644 x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/siem_migrations_product_features_config.ts diff --git a/x-pack/solutions/security/packages/features/actions.ts b/x-pack/solutions/security/packages/features/actions.ts new file mode 100644 index 0000000000000..3256ef5d618ff --- /dev/null +++ b/x-pack/solutions/security/packages/features/actions.ts @@ -0,0 +1,7 @@ +/* + * 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 * from './src/actions'; diff --git a/x-pack/solutions/security/packages/features/config.ts b/x-pack/solutions/security/packages/features/config.ts index 945623502029e..76939ed531e63 100644 --- a/x-pack/solutions/security/packages/features/config.ts +++ b/x-pack/solutions/security/packages/features/config.ts @@ -11,5 +11,6 @@ export { assistantDefaultProductFeaturesConfig } from './src/assistant/product_f export { attackDiscoveryDefaultProductFeaturesConfig } from './src/attack_discovery/product_feature_config'; export { timelineDefaultProductFeaturesConfig } from './src/timeline/product_feature_config'; export { notesDefaultProductFeaturesConfig } from './src/notes/product_feature_config'; +export { siemMigrationsDefaultProductFeaturesConfig } from './src/siem_migrations/product_feature_config'; export { createEnabledProductFeaturesConfigMap } from './src/helpers'; diff --git a/x-pack/solutions/security/packages/features/constants.ts b/x-pack/solutions/security/packages/features/constants.ts new file mode 100644 index 0000000000000..acf8656bfef83 --- /dev/null +++ b/x-pack/solutions/security/packages/features/constants.ts @@ -0,0 +1,7 @@ +/* + * 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 * from './src/constants'; diff --git a/x-pack/solutions/security/packages/features/product_features.ts b/x-pack/solutions/security/packages/features/product_features.ts index a6e2d252cee7b..683e43335a34b 100644 --- a/x-pack/solutions/security/packages/features/product_features.ts +++ b/x-pack/solutions/security/packages/features/product_features.ts @@ -11,3 +11,4 @@ export { getAssistantFeature } from './src/assistant'; export { getAttackDiscoveryFeature } from './src/attack_discovery'; export { getTimelineFeature } from './src/timeline'; export { getNotesFeature } from './src/notes'; +export { getSiemMigrationsFeature } from './src/siem_migrations'; diff --git a/x-pack/solutions/security/packages/features/src/actions.ts b/x-pack/solutions/security/packages/features/src/actions.ts new file mode 100644 index 0000000000000..3a8ccdb8bf7a2 --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/actions.ts @@ -0,0 +1,14 @@ +/* + * 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 { APP_ID } from './constants'; + +// The prefix ("securitySolution-") must be used by all the Security Solution API action privileges. +// This ensures product features are honored by the Kibana routes security authz. +export const API_ACTION_PREFIX = `${APP_ID}-`; + +export const SIEM_MIGRATIONS_API_ACTION_ALL = `${API_ACTION_PREFIX}siemMigrationsAll`; diff --git a/x-pack/solutions/security/packages/features/src/assistant/kibana_features.ts b/x-pack/solutions/security/packages/features/src/assistant/kibana_features.ts index 81cf7d18af129..511e6445127a4 100644 --- a/x-pack/solutions/security/packages/features/src/assistant/kibana_features.ts +++ b/x-pack/solutions/security/packages/features/src/assistant/kibana_features.ts @@ -20,7 +20,7 @@ export const getAssistantBaseKibanaFeature = (): BaseKibanaFeatureConfig => ({ defaultMessage: 'Elastic AI Assistant', } ), - order: 1100, + order: 1300, category: DEFAULT_APP_CATEGORIES.security, scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: [ASSISTANT_FEATURE_ID, 'kibana'], diff --git a/x-pack/solutions/security/packages/features/src/attack_discovery/kibana_features.ts b/x-pack/solutions/security/packages/features/src/attack_discovery/kibana_features.ts index 26f81b65213e0..1ac3f7b629ccb 100644 --- a/x-pack/solutions/security/packages/features/src/attack_discovery/kibana_features.ts +++ b/x-pack/solutions/security/packages/features/src/attack_discovery/kibana_features.ts @@ -20,7 +20,7 @@ export const getAttackDiscoveryBaseKibanaFeature = (): BaseKibanaFeatureConfig = defaultMessage: 'Attack discovery', } ), - order: 1100, + order: 1400, category: DEFAULT_APP_CATEGORIES.security, scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: [ATTACK_DISCOVERY_FEATURE_ID, 'kibana'], diff --git a/x-pack/solutions/security/packages/features/src/cases/v1_features/kibana_features.ts b/x-pack/solutions/security/packages/features/src/cases/v1_features/kibana_features.ts index 8721aebdd858a..0aa47c2694670 100644 --- a/x-pack/solutions/security/packages/features/src/cases/v1_features/kibana_features.ts +++ b/x-pack/solutions/security/packages/features/src/cases/v1_features/kibana_features.ts @@ -42,7 +42,7 @@ export const getCasesBaseKibanaFeature = ({ defaultMessage: 'Cases (Deprecated)', } ), - order: 1100, + order: 1200, category: DEFAULT_APP_CATEGORIES.security, scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: [CASES_FEATURE_ID, 'kibana'], diff --git a/x-pack/solutions/security/packages/features/src/cases/v2_features/kibana_features.ts b/x-pack/solutions/security/packages/features/src/cases/v2_features/kibana_features.ts index 8588712e1de38..2eb92cfd7ab4a 100644 --- a/x-pack/solutions/security/packages/features/src/cases/v2_features/kibana_features.ts +++ b/x-pack/solutions/security/packages/features/src/cases/v2_features/kibana_features.ts @@ -44,7 +44,7 @@ export const getCasesBaseKibanaFeatureV2 = ({ defaultMessage: 'Cases', } ), - order: 1100, + order: 1200, category: DEFAULT_APP_CATEGORIES.security, scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: [CASES_FEATURE_ID, 'kibana'], diff --git a/x-pack/solutions/security/packages/features/src/constants.ts b/x-pack/solutions/security/packages/features/src/constants.ts index 43d8bda53dc79..fa6d669ff1071 100644 --- a/x-pack/solutions/security/packages/features/src/constants.ts +++ b/x-pack/solutions/security/packages/features/src/constants.ts @@ -29,6 +29,7 @@ export const ASSISTANT_FEATURE_ID = 'securitySolutionAssistant' as const; export const ATTACK_DISCOVERY_FEATURE_ID = 'securitySolutionAttackDiscovery' as const; export const TIMELINE_FEATURE_ID = 'securitySolutionTimeline' as const; export const NOTES_FEATURE_ID = 'securitySolutionNotes' as const; +export const SIEM_MIGRATIONS_FEATURE_ID = 'securitySolutionSiemMigrations' as const; // Same as the plugin id defined by Cloud Security Posture export const CLOUD_POSTURE_APP_ID = 'csp' as const; diff --git a/x-pack/solutions/security/packages/features/src/product_features_keys.ts b/x-pack/solutions/security/packages/features/src/product_features_keys.ts index 7f32345c64990..7b2849565ca4b 100644 --- a/x-pack/solutions/security/packages/features/src/product_features_keys.ts +++ b/x-pack/solutions/security/packages/features/src/product_features_keys.ts @@ -132,6 +132,12 @@ export enum ProductFeatureNotesFeatureKey { */ notes = 'notes', } +export enum ProductFeatureSiemMigrationsKey { + /** + * Enables the SIEM Migrations main feature + */ + siemMigrations = 'siem_migrations', +} // Merges the two enums. export const ProductFeatureKey = { @@ -139,6 +145,7 @@ export const ProductFeatureKey = { ...ProductFeatureCasesKey, ...ProductFeatureAssistantKey, ...ProductFeatureAttackDiscoveryKey, + ...ProductFeatureSiemMigrationsKey, ...ProductFeatureTimelineFeatureKey, ...ProductFeatureNotesFeatureKey, }; @@ -148,6 +155,7 @@ export type ProductFeatureKeyType = | ProductFeatureCasesKey | ProductFeatureAssistantKey | ProductFeatureAttackDiscoveryKey + | ProductFeatureSiemMigrationsKey | ProductFeatureTimelineFeatureKey | ProductFeatureNotesFeatureKey; diff --git a/x-pack/solutions/security/packages/features/src/siem_migrations/index.ts b/x-pack/solutions/security/packages/features/src/siem_migrations/index.ts new file mode 100644 index 0000000000000..0fa2e897bb05a --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/siem_migrations/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { getSiemMigrationsBaseKibanaFeature } from './kibana_features'; +import type { ProductFeatureParams } from '../types'; + +export const getSiemMigrationsFeature = (): ProductFeatureParams => ({ + baseKibanaFeature: getSiemMigrationsBaseKibanaFeature(), + baseKibanaSubFeatureIds: [], + subFeaturesMap: new Map(), +}); diff --git a/x-pack/solutions/security/packages/features/src/siem_migrations/kibana_features.ts b/x-pack/solutions/security/packages/features/src/siem_migrations/kibana_features.ts new file mode 100644 index 0000000000000..5be49b28ba5ab --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/siem_migrations/kibana_features.ts @@ -0,0 +1,50 @@ +/* + * 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 { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; +import { i18n } from '@kbn/i18n'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; + +import { APP_ID, SIEM_MIGRATIONS_FEATURE_ID } from '../constants'; +import { type BaseKibanaFeatureConfig } from '../types'; + +export const getSiemMigrationsBaseKibanaFeature = (): BaseKibanaFeatureConfig => ({ + id: SIEM_MIGRATIONS_FEATURE_ID, + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.linkSecuritySolutionSiemMigrationsTitle', + { + defaultMessage: 'SIEM migrations', + } + ), + order: 1500, + category: DEFAULT_APP_CATEGORIES.security, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], + app: [SIEM_MIGRATIONS_FEATURE_ID, 'kibana'], + catalogue: [APP_ID], + minimumLicense: 'enterprise', + privileges: { + all: { + api: [], + app: [SIEM_MIGRATIONS_FEATURE_ID, 'kibana'], + catalogue: [APP_ID], + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + read: { + // No read-only mode currently supported + disabled: true, + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + }, +}); diff --git a/x-pack/solutions/security/packages/features/src/siem_migrations/product_feature_config.ts b/x-pack/solutions/security/packages/features/src/siem_migrations/product_feature_config.ts new file mode 100644 index 0000000000000..9db1be79228cf --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/siem_migrations/product_feature_config.ts @@ -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 { SIEM_MIGRATIONS_API_ACTION_ALL } from '../actions'; +import { ProductFeatureSiemMigrationsKey } from '../product_features_keys'; +import type { ProductFeatureKibanaConfig } from '../types'; + +/** + * App features privileges configuration for the Attack discovery feature. + * These are the configs that are shared between both offering types (ess and serverless). + * They can be extended on each offering plugin to register privileges using different way on each offering type. + * + * Privileges can be added in different ways: + * - `privileges`: the privileges that will be added directly into the main Security feature. + * - `subFeatureIds`: the ids of the sub-features that will be added into the Security subFeatures entry. + * - `subFeaturesPrivileges`: the privileges that will be added into the existing Security subFeature with the privilege `id` specified. + */ +export const siemMigrationsDefaultProductFeaturesConfig: Record< + ProductFeatureSiemMigrationsKey, + ProductFeatureKibanaConfig +> = { + [ProductFeatureSiemMigrationsKey.siemMigrations]: { + privileges: { + all: { + api: [SIEM_MIGRATIONS_API_ACTION_ALL], + ui: ['all'], + }, + }, + }, +}; diff --git a/x-pack/solutions/security/packages/features/src/types.ts b/x-pack/solutions/security/packages/features/src/types.ts index e180851a62cee..b40cace936e20 100644 --- a/x-pack/solutions/security/packages/features/src/types.ts +++ b/x-pack/solutions/security/packages/features/src/types.ts @@ -20,6 +20,7 @@ import type { AssistantSubFeatureId, CasesSubFeatureId, SecuritySubFeatureId, + ProductFeatureSiemMigrationsKey, ProductFeatureTimelineFeatureKey, ProductFeatureNotesFeatureKey, } from './product_features_keys'; @@ -69,6 +70,11 @@ export type ProductFeaturesNotesConfig = Map< ProductFeatureKibanaConfig >; +export type ProductFeaturesSiemMigrationsConfig = Map< + ProductFeatureSiemMigrationsKey, + ProductFeatureKibanaConfig +>; + export type AppSubFeaturesMap = Map; export interface ProductFeatureParams { @@ -76,3 +82,13 @@ export interface ProductFeatureParams { baseKibanaSubFeatureIds: T[]; subFeaturesMap: AppSubFeaturesMap; } + +export interface ProductFeaturesConfigurator { + security: () => ProductFeaturesConfig; + cases: () => ProductFeaturesConfig; + securityAssistant: () => ProductFeaturesConfig; + attackDiscovery: () => ProductFeaturesConfig; + timeline: () => ProductFeaturesConfig; + notes: () => ProductFeaturesConfig; + siemMigrations: () => ProductFeaturesConfig; +} diff --git a/x-pack/solutions/security/packages/side_nav/src/solution_side_nav_panel.test.tsx b/x-pack/solutions/security/packages/side_nav/src/solution_side_nav_panel.test.tsx index e5ec6efc41d72..2dd473fe92abd 100644 --- a/x-pack/solutions/security/packages/side_nav/src/solution_side_nav_panel.test.tsx +++ b/x-pack/solutions/security/packages/side_nav/src/solution_side_nav_panel.test.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { SolutionSideNavPanel, type SolutionSideNavPanelProps } from './solution_side_nav_panel'; -import { BETA_LABEL } from './beta_badge'; import { TELEMETRY_EVENT } from './telemetry/const'; import { METRIC_TYPE } from '@kbn/analytics'; import { TelemetryContextProvider } from './telemetry/telemetry_context'; @@ -48,8 +47,6 @@ const mockItems: SolutionSideNavItem[] = [ }, ]; -const betaMockItemsCount = mockItems.filter((item) => item.isBeta).length; - const mockCategories: LinkCategories = [ { label: 'HOSTS CATEGORY', @@ -98,7 +95,6 @@ describe('SolutionSideNavPanel', () => { mockItems.forEach((item) => { expect(result.getByText(item.label)).toBeInTheDocument(); }); - expect(result.queryAllByText(BETA_LABEL).length).toBe(betaMockItemsCount); }); it('should only render categories with items', () => { diff --git a/x-pack/solutions/security/packages/side_nav/src/solution_side_nav_panel.tsx b/x-pack/solutions/security/packages/side_nav/src/solution_side_nav_panel.tsx index 38cce27db1c44..394ad8bec3965 100644 --- a/x-pack/solutions/security/packages/side_nav/src/solution_side_nav_panel.tsx +++ b/x-pack/solutions/security/packages/side_nav/src/solution_side_nav_panel.tsx @@ -37,7 +37,6 @@ import { type SeparatorLinkCategory, } from '@kbn/security-solution-navigation'; import type { SolutionSideNavItem } from './types'; -import { BetaBadge } from './beta_badge'; import { TELEMETRY_EVENT } from './telemetry/const'; import { useTelemetryContext } from './telemetry/telemetry_context'; import { @@ -388,12 +387,11 @@ const SolutionSideNavPanelItem: React.FC = React. * Renders the navigation item label **/ const ItemLabel: React.FC<{ item: SolutionSideNavItem }> = React.memo(function ItemLabel({ - item: { label, openInNewTab, isBeta, betaOptions }, + item: { label, openInNewTab }, }) { return ( <> {label} {openInNewTab && } - {isBeta && } ); }); diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts index 1ed3a770d3410..67e4ca160e32d 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts @@ -374,6 +374,7 @@ import type { GetRuleMigrationIntegrationsResponse, GetRuleMigrationPrebuiltRulesRequestParamsInput, GetRuleMigrationPrebuiltRulesResponse, + GetRuleMigrationPrivilegesResponse, GetRuleMigrationResourcesRequestQueryInput, GetRuleMigrationResourcesRequestParamsInput, GetRuleMigrationResourcesResponse, @@ -391,6 +392,7 @@ import type { StartRuleMigrationResponse, StopRuleMigrationRequestParamsInput, StopRuleMigrationResponse, + UpdateRuleMigrationRequestParamsInput, UpdateRuleMigrationRequestBodyInput, UpdateRuleMigrationResponse, UpsertRuleMigrationResourcesRequestParamsInput, @@ -1491,6 +1493,21 @@ finalize it. }) .catch(catchAxiosErrorFormatAndThrow); } + /** + * Identifies the privileges required for a SIEM rules migration and returns the missing privileges + */ + async getRuleMigrationPrivileges() { + this.log.info(`${new Date().toISOString()} Calling API GetRuleMigrationPrivileges`); + return this.kbnClient + .request({ + path: '/internal/siem_migrations/rules/missing_privileges', + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '1', + }, + method: 'GET', + }) + .catch(catchAxiosErrorFormatAndThrow); + } /** * Retrieves resources for an existing SIEM rules migration */ @@ -2251,7 +2268,7 @@ detection engine rules. this.log.info(`${new Date().toISOString()} Calling API UpdateRuleMigration`); return this.kbnClient .request({ - path: '/internal/siem_migrations/rules', + path: replaceParams('/internal/siem_migrations/rules/{migration_id}', props.params), headers: { [ELASTIC_HTTP_VERSION_HEADER]: '1', }, @@ -2600,6 +2617,7 @@ export interface UpdateRuleProps { body: UpdateRuleRequestBodyInput; } export interface UpdateRuleMigrationProps { + params: UpdateRuleMigrationRequestParamsInput; body: UpdateRuleMigrationRequestBodyInput; } export interface UpdateWorkflowInsightProps { diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts index 898bc3910a84c..e04ca9a2ae2aa 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts @@ -31,6 +31,11 @@ export const SIEM_RULE_MIGRATION_RESOURCES_PATH = `${SIEM_RULE_MIGRATION_PATH}/r export const SIEM_RULE_MIGRATION_RESOURCES_MISSING_PATH = `${SIEM_RULE_MIGRATION_RESOURCES_PATH}/missing` as const; +export const SIEM_RULE_MIGRATION_MISSING_PRIVILEGES_PATH = + `${SIEM_RULE_MIGRATIONS_PATH}/missing_privileges` as const; + +export const LOOKUPS_INDEX_PREFIX = 'lookup_'; + export enum SiemMigrationTaskStatus { READY = 'ready', RUNNING = 'running', diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts index a03453b318aec..95e40f76f2d81 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts @@ -18,9 +18,9 @@ import { z } from '@kbn/zod'; import { ArrayFromString, BooleanFromString } from '@kbn/zod-helpers'; import { - UpdateRuleMigrationData, RuleMigrationTaskStats, OriginalRule, + UpdateRuleMigrationData, RuleMigration, RuleMigrationRetryFilter, RuleMigrationTranslationStats, @@ -113,6 +113,23 @@ export type GetRuleMigrationPrebuiltRulesResponse = z.infer< typeof GetRuleMigrationPrebuiltRulesResponse >; export const GetRuleMigrationPrebuiltRulesResponse = z.object({}).catchall(PrebuiltRuleVersion); + +/** + * The missing index privileges required for the migration + */ +export type GetRuleMigrationPrivilegesResponse = z.infer; +export const GetRuleMigrationPrivilegesResponse = z.array( + z.object({ + /** + * The index name of the privilege missing + */ + indexName: z.string(), + /** + * The index privileges level missing + */ + privileges: z.array(z.string()), + }) +); export type GetRuleMigrationResourcesRequestQuery = z.infer< typeof GetRuleMigrationResourcesRequestQuery >; @@ -250,6 +267,14 @@ export const StopRuleMigrationResponse = z.object({ stopped: z.boolean(), }); +export type UpdateRuleMigrationRequestParams = z.infer; +export const UpdateRuleMigrationRequestParams = z.object({ + migration_id: NonEmptyString, +}); +export type UpdateRuleMigrationRequestParamsInput = z.input< + typeof UpdateRuleMigrationRequestParams +>; + export type UpdateRuleMigrationRequestBody = z.infer; export const UpdateRuleMigrationRequestBody = z.array(UpdateRuleMigrationData); export type UpdateRuleMigrationRequestBodyInput = z.input; diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml index e3d9933ce6e93..263456a128558 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml @@ -4,36 +4,6 @@ info: version: '1' paths: # Rule migrations APIs - /internal/siem_migrations/rules: - put: - summary: Updates rules migrations - operationId: UpdateRuleMigration - x-codegen-enabled: true - x-internal: true - description: Updates rules migrations attributes - tags: - - SIEM Rule Migrations - requestBody: - required: true - content: - application/json: - schema: - type: array - items: - $ref: '../../rule_migration.schema.yaml#/components/schemas/UpdateRuleMigrationData' - responses: - 200: - description: Indicates rules migrations have been updated correctly. - content: - application/json: - schema: - type: object - required: - - updated - properties: - updated: - type: boolean - description: Indicates rules migrations have been updated. /internal/siem_migrations/rules/stats: get: @@ -113,6 +83,44 @@ paths: migration_id: description: The migration id created. $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' + + put: + summary: Updates rules of a migrations + operationId: UpdateRuleMigration + x-codegen-enabled: true + x-internal: true + description: Updates rules migrations attributes + tags: + - SIEM Rule Migrations + parameters: + - name: migration_id + in: path + required: true + schema: + description: The migration id to start + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + $ref: '../../rule_migration.schema.yaml#/components/schemas/UpdateRuleMigrationData' + responses: + 200: + description: Indicates rules migrations have been updated correctly. + content: + application/json: + schema: + type: object + required: + - updated + properties: + updated: + type: boolean + description: Indicates rules migrations have been updated. + get: summary: Retrieves all the rules of a migration operationId: GetRuleMigration @@ -535,3 +543,35 @@ paths: description: The identified resources missing items: $ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigrationResourceBase' + + /internal/siem_migrations/rules/missing_privileges: + get: + summary: Retrieves the missing privileges for a migration + operationId: GetRuleMigrationPrivileges + x-codegen-enabled: true + x-internal: true + description: Identifies the privileges required for a SIEM rules migration and returns the missing privileges + tags: + - SIEM Rule Migrations + responses: + 200: + description: Indicates privileges have been retrieved correctly. + content: + application/json: + schema: + type: array + description: The missing index privileges required for the migration + items: + type: object + required: + - indexName + - privileges + properties: + indexName: + type: string + description: The index name of the privilege missing + privileges: + type: array + items: + type: string + description: The index privileges level missing diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/capabilities/capabilities_checker.ts b/x-pack/solutions/security/plugins/security_solution/public/common/lib/capabilities/capabilities_checker.ts new file mode 100644 index 0000000000000..3df0a520ec428 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/capabilities/capabilities_checker.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Capabilities } from '@kbn/core/public'; +import { hasCapabilities, type RequiredCapabilities } from './has_capabilities'; + +/** + * class to check if capabilities are granted using the `RequiredCapabilities` format. + */ +export class CapabilitiesChecker { + constructor(private readonly capabilities: Capabilities) {} + public has(requiredCapabilities: RequiredCapabilities): boolean { + return hasCapabilities(this.capabilities, requiredCapabilities); + } +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/capabilities/has_capabilities.ts b/x-pack/solutions/security/plugins/security_solution/public/common/lib/capabilities/has_capabilities.ts index 97a1dc1231f56..7a533605a521a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/lib/capabilities/has_capabilities.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/capabilities/has_capabilities.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { get, isArray } from 'lodash'; +import get from 'lodash/get'; +import isArray from 'lodash/isArray'; import type { Capabilities } from '@kbn/core/public'; /** diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/capabilities/index.ts b/x-pack/solutions/security/plugins/security_solution/public/common/lib/capabilities/index.ts index 22bc4e2d4fae4..52216af616321 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/lib/capabilities/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/capabilities/index.ts @@ -6,3 +6,4 @@ */ export { hasCapabilities, type RequiredCapabilities } from './has_capabilities'; +export { CapabilitiesChecker } from './capabilities_checker'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/hooks/use_stored_state.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/hooks/use_stored_state.ts index 98f865741d4a9..86ef735d13703 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/hooks/use_stored_state.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/hooks/use_stored_state.ts @@ -86,4 +86,7 @@ export const useStoredIntegrationSearchTerm = (spaceId: string) => * Stores the integration search term per space */ export const useStoredAssistantConnectorId = (spaceId: string) => - useDefinedLocalStorage(`${LocalStorageKey.assistantConnectorId}.${spaceId}`, null); + useDefinedLocalStorage( + `${LocalStorageKey.assistantConnectorId}.${spaceId}`, + undefined + ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding.tsx index 79ea7792386d1..38076da2245bd 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding.tsx @@ -7,24 +7,17 @@ import React from 'react'; -import { Routes, Route } from '@kbn/shared-ux-router'; import { EuiSpacer, useEuiTheme } from '@elastic/eui'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; -import { Redirect } from 'react-router-dom'; -import { ONBOARDING_PATH } from '../../../common/constants'; import { PluginTemplateWrapper } from '../../common/components/plugin_template_wrapper'; import { CenteredLoadingSpinner } from '../../common/components/centered_loading_spinner'; import { useSpaceId } from '../../common/hooks/use_space_id'; -import { OnboardingTopicId, PAGE_CONTENT_WIDTH } from '../constants'; +import { PAGE_CONTENT_WIDTH } from '../constants'; import { OnboardingContextProvider } from './onboarding_context'; import { OnboardingAVCBanner } from './onboarding_banner'; -import { OnboardingRoute } from './onboarding_route'; +import { OnboardingRouter } from './onboarding_router'; import { OnboardingFooter } from './onboarding_footer'; -const topicPathParam = `:topicId(${Object.values(OnboardingTopicId) // any topics - .filter((val) => val !== OnboardingTopicId.default) // except "default" - .join('|')})?`; // optional parameter - export const OnboardingPage = React.memo(() => { const spaceId = useSpaceId(); const { euiTheme } = useEuiTheme(); @@ -48,14 +41,7 @@ export const OnboardingPage = React.memo(() => { bottomBorder="extended" style={{ backgroundColor: euiTheme.colors.backgroundBaseSubdued }} > - - - } /> - + diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_card.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_card.tsx index 10e3690a63a32..a8e8b12ee7085 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_card.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_card.tsx @@ -18,15 +18,15 @@ import { CenteredLoadingSpinner } from '../../../../../common/components/centere import { OnboardingCardId } from '../../../../constants'; import type { OnboardingCardComponent } from '../../../../types'; import * as i18n from './translations'; +import { ConnectorsMissingPrivilegesCallOut } from '../common/connectors/missing_privileges'; import { useStoredAssistantConnectorId } from '../../../hooks/use_stored_state'; import { useOnboardingContext } from '../../../onboarding_context'; import { OnboardingCardContentPanel } from '../common/card_content_panel'; import { ConnectorCards } from '../common/connectors/connector_cards'; import { CardCallOut } from '../common/card_callout'; import { CardSubduedText } from '../common/card_subdued_text'; -import type { AssistantCardMetadata } from './types'; -import { MissingPrivilegesCallOut } from '../common/connectors/missing_privileges'; import type { AIConnector } from '../common/connectors/types'; +import type { AssistantCardMetadata } from './types'; export const AssistantCard: OnboardingCardComponent = ({ isCardComplete, @@ -188,7 +188,7 @@ export const AssistantCard: OnboardingCardComponent = ({ ) : ( - + )} ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_cards.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_cards.tsx index 7c7ba2b31557a..274772cb5d2ec 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_cards.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_cards.tsx @@ -5,20 +5,22 @@ * 2.0. */ -import React, { useCallback } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiCallOut } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; import { css } from '@emotion/react'; +import { useLoadActionTypes } from '@kbn/elastic-assistant/impl/connectorland/use_load_action_types'; +import { useKibana } from '../../../../../../common/lib/kibana/kibana_react'; +import { ConnectorsMissingPrivilegesCallOut } from './missing_privileges'; import type { AIConnector } from './types'; -import * as i18n from './translations'; -import { MissingPrivilegesDescription } from './missing_privileges'; import { ConnectorSetup } from './connector_setup'; import { ConnectorSelectorPanel } from './connector_selector_panel'; +import { AIActionTypeIds } from './constants'; interface ConnectorCardsProps { onNewConnectorSaved: (connectorId: string) => void; canCreateConnectors?: boolean; connectors?: AIConnector[]; // make connectors optional to handle loading state - selectedConnectorId?: string | null; + selectedConnectorId?: string; onConnectorSelected: (connector: AIConnector) => void; } @@ -30,6 +32,13 @@ export const ConnectorCards = React.memo( selectedConnectorId, onConnectorSelected, }) => { + const { http, notifications } = useKibana().services; + const { data } = useLoadActionTypes({ http, toasts: notifications.toasts }); + const actionTypes = useMemo( + () => data?.filter(({ id }) => AIActionTypeIds.includes(id)), + [data] + ); + const onNewConnectorStoredSave = useCallback( (newConnector: AIConnector) => { onNewConnectorSaved(newConnector.id); @@ -39,7 +48,7 @@ export const ConnectorCards = React.memo( [onConnectorSelected, onNewConnectorSaved] ); - if (!connectors) { + if (!connectors || !actionTypes) { return ; } @@ -47,11 +56,7 @@ export const ConnectorCards = React.memo( // show callout when user is missing actions.save privilege if (!hasConnectors && !canCreateConnectors) { - return ( - - - - ); + return ; } return ( @@ -71,7 +76,7 @@ export const ConnectorCards = React.memo( )} - + diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_selector_panel.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_selector_panel.tsx index 149b65e62a458..01ad467b0086d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_selector_panel.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_selector_panel.tsx @@ -5,21 +5,65 @@ * 2.0. */ -import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; +import React, { useMemo, useEffect, useCallback } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiPanel, EuiText } from '@elastic/eui'; import { css } from '@emotion/react'; -import { ConnectorSelectorWithIcon } from './connector_selector_with_icon'; -import * as i18n from './translations'; +import { ConnectorSelector } from '@kbn/security-solution-connectors'; +import { + getActionTypeTitle, + getGenAiConfig, +} from '@kbn/elastic-assistant/impl/connectorland/helpers'; +import { useKibana } from '../../../../../../common/lib/kibana/kibana_react'; import type { AIConnector } from './types'; +import * as i18n from './translations'; interface ConnectorSelectorPanelProps { connectors: AIConnector[]; - selectedConnectorId?: string | null; + selectedConnectorId?: string; onConnectorSelected: (connector: AIConnector) => void; } export const ConnectorSelectorPanel = React.memo( ({ connectors, selectedConnectorId, onConnectorSelected }) => { + const { actionTypeRegistry } = useKibana().services.triggersActionsUi; + + const selectedConnector = useMemo( + () => connectors.find((connector) => connector.id === selectedConnectorId), + [connectors, selectedConnectorId] + ); + + useEffect(() => { + if (connectors.length === 1) { + onConnectorSelected(connectors[0]); + } + }, [connectors, onConnectorSelected]); + + const connectorOptions = useMemo( + () => + connectors.map((connector) => { + let description: string; + if (connector.isPreconfigured) { + description = i18n.PRECONFIGURED_CONNECTOR; + } else { + description = + getGenAiConfig(connector)?.apiProvider ?? + getActionTypeTitle(actionTypeRegistry.get(connector.actionTypeId)); + } + return { id: connector.id, name: connector.name, description }; + }), + [actionTypeRegistry, connectors] + ); + + const onConnectorSelectionChange = useCallback( + (connectorId: string) => { + const connector = connectors.find((c) => c.id === connectorId); + if (connector) { + onConnectorSelected(connector); + } + }, + [connectors, onConnectorSelected] + ); + return ( ( {i18n.SELECTED_PROVIDER} - + + {selectedConnector && ( + + + + )} + + + + ); } ); - ConnectorSelectorPanel.displayName = 'ConnectorSelectorPanel'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_selector_with_icon.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_selector_with_icon.tsx deleted file mode 100644 index bbfbd56f2fac7..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_selector_with_icon.tsx +++ /dev/null @@ -1,118 +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 { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; -import React, { useMemo, useEffect, useCallback } from 'react'; -import { useAssistantContext } from '@kbn/elastic-assistant'; -import { ConnectorSelector } from '@kbn/security-solution-connectors'; -import { - getActionTypeTitle, - getGenAiConfig, -} from '@kbn/elastic-assistant/impl/connectorland/helpers'; -import { css } from '@emotion/react'; -import type { AIConnector } from './types'; -import { useFilteredActionTypes } from './hooks/use_load_action_types'; -import * as i18n from './translations'; - -interface Props { - isDisabled?: boolean; - selectedConnectorId?: string | null; - connectors: AIConnector[]; - onConnectorSelected: (connector: AIConnector) => void; -} - -/** - * A compact wrapper of the ConnectorSelector with a Selected Icon - */ -export const ConnectorSelectorWithIcon = React.memo( - ({ isDisabled = false, selectedConnectorId, connectors, onConnectorSelected }) => { - const { actionTypeRegistry, assistantAvailability } = useAssistantContext(); - - const actionTypes = useFilteredActionTypes(); - - const selectedConnector = useMemo( - () => connectors.find((connector) => connector.id === selectedConnectorId), - [connectors, selectedConnectorId] - ); - - useEffect(() => { - if (connectors.length === 1) { - onConnectorSelected(connectors[0]); - } - }, [connectors, onConnectorSelected]); - - const localIsDisabled = isDisabled || !assistantAvailability.hasConnectorsReadPrivilege; - - const connectorOptions = useMemo( - () => - (connectors ?? []).map((connector) => { - const connectorTypeTitle = - getGenAiConfig(connector)?.apiProvider ?? - getActionTypeTitle(actionTypeRegistry.get(connector.actionTypeId)); - const connectorDetails = connector.isPreconfigured - ? i18n.PRECONFIGURED_CONNECTOR - : connectorTypeTitle; - - return { - id: connector.id, - name: connector.name, - description: connectorDetails, - }; - }), - [actionTypeRegistry, connectors] - ); - - const onConnectorSelectionChange = useCallback( - (connectorId: string) => { - const connector = (connectors ?? []).find((c) => c.id === connectorId); - if (connector) { - onConnectorSelected(connector); - } - }, - [connectors, onConnectorSelected] - ); - - if (!actionTypes) { - return ; - } - - return ( - - {selectedConnector && ( - - - - )} - {selectedConnectorId && ( - - - - )} - - ); - } -); - -ConnectorSelectorWithIcon.displayName = 'ConnectorSelectorWithIcon'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_setup.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_setup.tsx index 16086bf5eb73d..a59a8545608c4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_setup.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_setup.tsx @@ -7,104 +7,89 @@ import React, { useCallback, useState } from 'react'; import { type ActionConnector } from '@kbn/triggers-actions-ui-plugin/public/common/constants'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiPanel, - EuiLoadingSpinner, - EuiButton, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiPanel, EuiButton } from '@elastic/eui'; import { css } from '@emotion/css'; import type { ActionType } from '@kbn/actions-plugin/common'; import { AddConnectorModal } from '@kbn/elastic-assistant/impl/connectorland/add_connector_modal'; import * as i18n from './translations'; import { useKibana } from '../../../../../../common/lib/kibana'; -import { useFilteredActionTypes } from './hooks/use_load_action_types'; interface ConnectorSetupProps { - onConnectorSaved?: (savedAction: ActionConnector) => void; - onClose?: () => void; + actionTypes: ActionType[]; + onConnectorSaved: (savedAction: ActionConnector) => void; } -export const ConnectorSetup = React.memo(({ onConnectorSaved, onClose }) => { - const [isModalVisible, setIsModalVisible] = useState(false); - const [selectedActionType, setSelectedActionType] = useState(null); +export const ConnectorSetup = React.memo( + ({ onConnectorSaved, actionTypes }) => { + const [isModalVisible, setIsModalVisible] = useState(false); + const [selectedActionType, setSelectedActionType] = useState(null); - const { - triggersActionsUi: { actionTypeRegistry }, - } = useKibana().services; + const { actionTypeRegistry } = useKibana().services.triggersActionsUi; - const onModalClose = useCallback(() => { - setSelectedActionType(null); - setIsModalVisible(false); - onClose?.(); - }, [onClose]); + const onModalClose = useCallback(() => { + setSelectedActionType(null); + setIsModalVisible(false); + }, []); - const actionTypes = useFilteredActionTypes(); - - if (!actionTypes) { - return ; + return ( + <> + + + + + {actionTypes.map((actionType: ActionType) => ( + + + + + + + + ))} + + + + setIsModalVisible(true)} + isLoading={false} + > + {i18n.CREATE_NEW_CONNECTOR_BUTTON} + + + + + {isModalVisible && ( + + )} + + ); } - - return ( - <> - - - - - {actionTypes.map((actionType: ActionType) => ( - - - - - - - - ))} - - - - setIsModalVisible(true)} - isLoading={false} - > - {i18n.CREATE_NEW_CONNECTOR_BUTTON} - - - - - {isModalVisible && onConnectorSaved && ( - - )} - - ); -}); +); ConnectorSetup.displayName = 'ConnectorSetup'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/hooks/use_load_action_types.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/hooks/use_load_action_types.ts deleted file mode 100644 index 5c1c18df71a45..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/hooks/use_load_action_types.ts +++ /dev/null @@ -1,17 +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 { useMemo } from 'react'; -import { useLoadActionTypes as loadActionTypes } from '@kbn/elastic-assistant/impl/connectorland/use_load_action_types'; -import { useAssistantContext } from '@kbn/elastic-assistant'; -import { AIActionTypeIds } from '../constants'; - -export const useFilteredActionTypes = () => { - const { http, toasts } = useAssistantContext(); - const { data } = loadActionTypes({ http, toasts }); - return useMemo(() => data?.filter(({ id }) => AIActionTypeIds.includes(id)), [data]); -}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/missing_privileges.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/missing_privileges.tsx index 40e211d857680..28830c133431f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/missing_privileges.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/missing_privileges.tsx @@ -5,45 +5,24 @@ * 2.0. */ import React from 'react'; -import { EuiCallOut, EuiCode, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import * as i18n from './translations'; +import { MissingPrivilegesCallOut, MissingPrivilegesDescription } from '../missing_privileges'; -export const MissingPrivilegesDescription = React.memo(() => { - return ( - - {i18n.PRIVILEGES_REQUIRED_TITLE} - - -
    -
  • {i18n.REQUIRED_PRIVILEGES_CONNECTORS_ALL}
  • -
-
-
- {i18n.CONTACT_ADMINISTRATOR} -
- ); -}); -MissingPrivilegesDescription.displayName = 'MissingPrivilegesDescription'; +const LEVEL_TRANSLATION = { + read: i18n.REQUIRED_PRIVILEGES_CONNECTORS_READ, + all: i18n.REQUIRED_PRIVILEGES_CONNECTORS_ALL, +}; -interface MissingPrivilegesTooltip { - children: React.ReactElement; // EuiToolTip requires a single ReactElement child -} -export const MissingPrivilegesTooltip = React.memo(({ children }) => ( - } - > - {children} - -)); -MissingPrivilegesTooltip.displayName = 'MissingPrivilegesTooltip'; +export const ConnectorsMissingPrivilegesDescription = React.memo<{ level: 'read' | 'all' }>( + ({ level }) => +); +ConnectorsMissingPrivilegesDescription.displayName = 'ConnectorsMissingPrivilegesDescription'; -export const MissingPrivilegesCallOut = React.memo(() => { - return ( - - - - ); -}); -MissingPrivilegesCallOut.displayName = 'MissingPrivilegesCallOut'; +export const ConnectorsMissingPrivilegesCallOut = React.memo<{ level: 'read' | 'all' }>( + ({ level }) => ( + + + + ) +); +ConnectorsMissingPrivilegesCallOut.displayName = 'ConnectorsMissingPrivilegesCallOut'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/translations.ts index a9ba5ac647ff8..c12788eafbfa2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/translations.ts @@ -20,13 +20,6 @@ export const SELECTED_PROVIDER = i18n.translate( } ); -export const PRIVILEGES_MISSING_TITLE = i18n.translate( - 'xpack.securitySolution.onboarding.assistantCard.missingPrivileges.title', - { - defaultMessage: 'Missing privileges', - } -); - export const PRECONFIGURED_CONNECTOR = i18n.translate( 'xpack.securitySolution.onboarding.assistantCard.preconfiguredTitle', { @@ -34,23 +27,11 @@ export const PRECONFIGURED_CONNECTOR = i18n.translate( } ); -export const PRIVILEGES_REQUIRED_TITLE = i18n.translate( - 'xpack.securitySolution.onboarding.assistantCard.requiredPrivileges', - { - defaultMessage: 'The minimum Kibana privileges required to use this feature are:', - } +export const REQUIRED_PRIVILEGES_CONNECTORS_READ = i18n.translate( + 'xpack.securitySolution.onboarding.assistantCard.requiredPrivileges.connectorsRead', + { defaultMessage: 'Management > Actions & Connectors: Read' } ); - export const REQUIRED_PRIVILEGES_CONNECTORS_ALL = i18n.translate( 'xpack.securitySolution.onboarding.assistantCard.requiredPrivileges.connectorsAll', - { - defaultMessage: 'Management > Connectors: All', - } -); - -export const CONTACT_ADMINISTRATOR = i18n.translate( - 'xpack.securitySolution.onboarding.assistantCard.missingPrivileges.contactAdministrator', - { - defaultMessage: 'Contact your administrator for assistance.', - } + { defaultMessage: 'Management > Actions & Connectors: All' } ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/missing_privileges/index.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/missing_privileges/index.ts new file mode 100644 index 0000000000000..d2edd163e79df --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/missing_privileges/index.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. + */ + +export { + MissingPrivilegesDescription, + MissingPrivilegesCallOut, + MissingPrivilegesTooltip, +} from './missing_privileges'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/missing_privileges/missing_privileges.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/missing_privileges/missing_privileges.tsx new file mode 100644 index 0000000000000..65ee7124e972f --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/missing_privileges/missing_privileges.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { PropsWithChildren } from 'react'; +import React from 'react'; +import { + EuiCallOut, + EuiCode, + EuiFlexGroup, + EuiFlexItem, + EuiToolTip, + useEuiTheme, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +import * as i18n from './translations'; + +export const MissingPrivilegesDescription = React.memo<{ privileges: string[] }>( + ({ privileges }) => { + return ( + + {i18n.PRIVILEGES_REQUIRED_TITLE} + + +
    + {privileges.map((privilege) => ( +
  • {privilege}
  • + ))} +
+
+
+ {i18n.CONTACT_ADMINISTRATOR} +
+ ); + } +); +MissingPrivilegesDescription.displayName = 'MissingPrivilegesDescription'; + +interface MissingPrivilegesTooltip { + children: React.ReactElement; // EuiToolTip requires a single ReactElement child + description: React.ReactNode; +} +export const MissingPrivilegesTooltip = React.memo( + ({ children, description }) => ( + + {children} + + ) +); +MissingPrivilegesTooltip.displayName = 'MissingPrivilegesTooltip'; + +export const MissingPrivilegesCallOut = React.memo>(({ children }) => { + const { euiTheme } = useEuiTheme(); + const calloutCss = css` + border-radius: ${euiTheme.border.radius.small}; + border: 1px solid ${euiTheme.colors.borderBaseSubdued}; + `; + return ( + + {children} + + ); +}); +MissingPrivilegesCallOut.displayName = 'MissingPrivilegesCallOut'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/missing_privileges/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/missing_privileges/translations.ts new file mode 100644 index 0000000000000..79e48d3d09814 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/missing_privileges/translations.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 { i18n } from '@kbn/i18n'; + +export const PRIVILEGES_MISSING_TITLE = i18n.translate( + 'xpack.securitySolution.onboarding.assistantCard.missingPrivileges.title', + { + defaultMessage: 'Missing privileges', + } +); + +export const PRIVILEGES_REQUIRED_TITLE = i18n.translate( + 'xpack.securitySolution.onboarding.assistantCard.requiredPrivileges', + { + defaultMessage: 'The minimum Kibana privileges required to use this feature are:', + } +); + +export const CONTACT_ADMINISTRATOR = i18n.translate( + 'xpack.securitySolution.onboarding.assistantCard.missingPrivileges.contactAdministrator', + { + defaultMessage: 'Contact your administrator for assistance.', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx index ccb98aef00c84..c2389382cce84 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx @@ -15,9 +15,9 @@ import * as i18n from './translations'; import { OnboardingCardContentPanel } from '../../common/card_content_panel'; import { ConnectorCards } from '../../common/connectors/connector_cards'; import { CardSubduedText } from '../../common/card_subdued_text'; -import type { AIConnectorCardMetadata } from './types'; -import { MissingPrivilegesCallOut } from '../../common/connectors/missing_privileges'; +import { ConnectorsMissingPrivilegesCallOut } from '../../common/connectors/missing_privileges'; import type { AIConnector } from '../../common/connectors/types'; +import type { AIConnectorCardMetadata } from './types'; export const AIConnectorCard: OnboardingCardComponent = ({ checkCompleteMetadata, @@ -25,9 +25,9 @@ export const AIConnectorCard: OnboardingCardComponent = setComplete, }) => { const { siemMigrations } = useKibana().services; - const [storedConnectorId, setStoredConnectorId] = useDefinedLocalStorage( + const [storedConnectorId, setStoredConnectorId] = useDefinedLocalStorage( siemMigrations.rules.connectorIdStorage.key, - '' + undefined ); const setSelectedConnector = useCallback( (connector: AIConnector) => { @@ -65,7 +65,7 @@ export const AIConnectorCard: OnboardingCardComponent = ) : ( - + )} ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/connectors_check_complete.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/connectors_check_complete.ts index e37b3ada95575..5d8728b892a6d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/connectors_check_complete.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/connectors_check_complete.ts @@ -7,6 +7,7 @@ import { loadAllActions as loadConnectors } from '@kbn/triggers-actions-ui-plugin/public/common/constants'; import type { AIConnector } from '@kbn/elastic-assistant/impl/connectorland/connector_selector'; +import { CapabilitiesChecker } from '../../../../../../common/lib/capabilities/capabilities_checker'; import type { OnboardingCardCheckComplete } from '../../../../../types'; import { AIActionTypeIds } from '../../common/connectors/constants'; import type { AIConnectorCardMetadata } from './types'; @@ -15,10 +16,18 @@ export const checkAiConnectorsCardComplete: OnboardingCardCheckComplete< AIConnectorCardMetadata > = async ({ http, application, siemMigrations }) => { let isComplete = false; + const capabilities = new CapabilitiesChecker(application.capabilities); + + const canExecuteConnectors = capabilities.has([['actions.show', 'actions.execute']]); + const canCreateConnectors = capabilities.has('actions.save'); + + if (!capabilities.has('actions.show')) { + return { isComplete, metadata: { connectors: [], canExecuteConnectors, canCreateConnectors } }; + } + const allConnectors = await loadConnectors({ http }); - const { capabilities } = application; - const aiConnectors = allConnectors.reduce((acc: AIConnector[], connector) => { + const connectors = allConnectors.reduce((acc, connector) => { if (!connector.isMissingSecrets && AIActionTypeIds.includes(connector.actionTypeId)) { acc.push(connector); } @@ -27,19 +36,12 @@ export const checkAiConnectorsCardComplete: OnboardingCardCheckComplete< const storedConnectorId = siemMigrations.rules.connectorIdStorage.get(); if (storedConnectorId) { - if (aiConnectors.length === 0) { + if (connectors.length === 0) { siemMigrations.rules.connectorIdStorage.remove(); } else { - isComplete = aiConnectors.some((connector) => connector.id === storedConnectorId); + isComplete = connectors.some((connector) => connector.id === storedConnectorId); } } - return { - isComplete, - metadata: { - connectors: aiConnectors, - canExecuteConnectors: Boolean(capabilities.actions?.show && capabilities.actions?.execute), - canCreateConnectors: Boolean(capabilities.actions?.save), - }, - }; + return { isComplete, metadata: { connectors, canExecuteConnectors, canCreateConnectors } }; }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/index.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/index.ts index fcf950e0840e9..7847e81b0f98e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/index.ts @@ -10,12 +10,14 @@ import type { OnboardingCardConfig } from '../../../../../types'; import { OnboardingCardId } from '../../../../../constants'; import { START_MIGRATION_CARD_TITLE } from './translations'; import cardIcon from './images/card_header_icon.png'; +import type { StartMigrationCardMetadata } from './types'; import { checkStartMigrationCardComplete } from './start_migration_check_complete'; -export const startMigrationCardConfig: OnboardingCardConfig = { +export const startMigrationCardConfig: OnboardingCardConfig = { id: OnboardingCardId.siemMigrationsStart, title: START_MIGRATION_CARD_TITLE, icon: cardIcon, + licenseTypeRequired: 'enterprise', Component: React.lazy( () => import( @@ -24,5 +26,4 @@ export const startMigrationCardConfig: OnboardingCardConfig = { ) ), checkComplete: checkStartMigrationCardComplete, - licenseTypeRequired: 'enterprise', }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx index baebbde53b4cf..34e7a25d9a125 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx @@ -15,11 +15,16 @@ import { useLatestStats } from '../../../../../../siem_migrations/rules/service/ import { CenteredLoadingSpinner } from '../../../../../../common/components/centered_loading_spinner'; import type { OnboardingCardComponent } from '../../../../../types'; import { OnboardingCardContentPanel } from '../../common/card_content_panel'; +import type { StartMigrationCardMetadata } from './types'; import { RuleMigrationsPanels } from './rule_migrations_panels'; import { useStyles } from './start_migration_card.styles'; import * as i18n from './translations'; +import { + MissingPrivilegesCallOut, + MissingPrivilegesDescription, +} from '../../common/missing_privileges'; -export const StartMigrationCard: OnboardingCardComponent = React.memo( +const StartMigrationsBody: OnboardingCardComponent = React.memo( ({ setComplete, isCardComplete, setExpandedCardId }) => { const styles = useStyles(); const { data: migrationsStats, isLoading, refreshStats } = useLatestStats(); @@ -63,6 +68,28 @@ export const StartMigrationCard: OnboardingCardComponent = React.memo( ); } ); +StartMigrationsBody.displayName = 'StartMigrationsBody'; + +export const StartMigrationCard: OnboardingCardComponent = React.memo( + ({ checkCompleteMetadata, ...props }) => { + if (!checkCompleteMetadata) { + return ; + } + + const { missingCapabilities } = checkCompleteMetadata; + if (missingCapabilities.length > 0) { + return ( + + + + + + ); + } + + return ; + } +); StartMigrationCard.displayName = 'StartMigrationCard'; // eslint-disable-next-line import/no-default-export diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts index 79a7238b9554e..2e3a8f238ac43 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts @@ -6,14 +6,25 @@ */ import { SiemMigrationTaskStatus } from '../../../../../../../common/siem_migrations/constants'; + import type { OnboardingCardCheckComplete } from '../../../../../types'; +import type { StartMigrationCardMetadata } from './types'; + +export const checkStartMigrationCardComplete: OnboardingCardCheckComplete< + StartMigrationCardMetadata +> = async ({ siemMigrations }) => { + const missingCapabilities = siemMigrations.rules + .getMissingCapabilities('all') + .map(({ description }) => description); + + let isComplete = false; + + if (missingCapabilities.length === 0) { + const migrationsStats = await siemMigrations.rules.getRuleMigrationsStats(); + isComplete = migrationsStats.some( + (migrationStats) => migrationStats.status === SiemMigrationTaskStatus.FINISHED + ); + } -export const checkStartMigrationCardComplete: OnboardingCardCheckComplete = async ({ - siemMigrations, -}) => { - const migrationsStats = await siemMigrations.rules.getRuleMigrationsStats(); - const isComplete = migrationsStats.some( - (migrationStats) => migrationStats.status === SiemMigrationTaskStatus.FINISHED - ); - return isComplete; + return { isComplete, metadata: { missingCapabilities } }; }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/types.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/types.ts new file mode 100644 index 0000000000000..065fa1df66e9d --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/types.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface StartMigrationCardMetadata { + missingCapabilities: string[]; +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_route.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_route.tsx deleted file mode 100644 index 6e7dca524ce81..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_route.tsx +++ /dev/null @@ -1,38 +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 React, { useEffect } from 'react'; - -import type { RouteComponentProps } from 'react-router-dom'; -import { OnboardingHeader } from './onboarding_header'; -import { OnboardingBody } from './onboarding_body'; -import type { OnboardingRouteParams } from '../types'; -import { getCardIdFromHash, useUrlDetail } from './hooks/use_url_detail'; - -type OnboardingRouteProps = RouteComponentProps; - -export const OnboardingRoute = React.memo(({ match, location }) => { - const { syncUrlDetails } = useUrlDetail(); - - /** - * This effect syncs the URL details with the stored state, it only needs to be executed once per page load. - */ - useEffect(() => { - const pathTopicId = match.params.topicId || null; - const hashCardId = getCardIdFromHash(location.hash); - syncUrlDetails(pathTopicId, hashCardId); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( - <> - - - - ); -}); -OnboardingRoute.displayName = 'OnboardingContent'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_router.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_router.tsx new file mode 100644 index 0000000000000..31d51b9427d46 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_router.tsx @@ -0,0 +1,66 @@ +/* + * 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, { useEffect, useMemo } from 'react'; + +import type { RouteComponentProps } from 'react-router-dom'; +import { Routes, Route } from '@kbn/shared-ux-router'; +import { Redirect } from 'react-router-dom'; +import { ONBOARDING_PATH } from '../../../common/constants'; +import type { OnboardingRouteParams } from '../types'; +import { OnboardingTopicId } from '../constants'; +import { getCardIdFromHash, useUrlDetail } from './hooks/use_url_detail'; +import { useOnboardingContext } from './onboarding_context'; +import { OnboardingHeader } from './onboarding_header'; +import { OnboardingBody } from './onboarding_body'; + +export const OnboardingRouter = React.memo(() => { + const { config } = useOnboardingContext(); + + const topicPathParam = useMemo(() => { + const availableTopics = [...config.values()] + .map(({ id }) => id) // available topic ids + .filter((val) => val !== OnboardingTopicId.default) // except "default" + .join('|'); + if (availableTopics) { + return `/:topicId(${availableTopics})?`; // optional parameter} + } + return ''; // only default topic available, no need for topic path parameter + }, [config]); + + return ( + + + } /> + + ); +}); +OnboardingRouter.displayName = 'OnboardingRouter'; + +type OnboardingRouteProps = RouteComponentProps; + +const OnboardingRoute = React.memo(({ match, location }) => { + const { syncUrlDetails } = useUrlDetail(); + + /** + * This effect syncs the URL details with the stored state, it only needs to be executed once per page load. + */ + useEffect(() => { + const pathTopicId = match.params.topicId || null; + const hashCardId = getCardIdFromHash(location.hash); + syncUrlDetails(pathTopicId, hashCardId); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + <> + + + + ); +}); +OnboardingRoute.displayName = 'OnboardingContent'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/config.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/config.ts index 4c70525f694bf..c424f28cef71b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/config.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/config.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { SIEM_MIGRATIONS_FEATURE_ID } from '@kbn/security-solution-features/constants'; import { OnboardingTopicId } from './constants'; import { defaultBodyConfig, @@ -28,6 +29,7 @@ export const onboardingConfig: TopicConfig[] = [ }), body: siemMigrationsBodyConfig, licenseTypeRequired: 'enterprise', + capabilitiesRequired: `${SIEM_MIGRATIONS_FEATURE_ID}.all`, disabledExperimentalFlagRequired: 'siemMigrationsDisabled', }, ]; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/links.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/links.ts index 6fe2b02e62233..df1f88d1b4266 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/links.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/links.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { SIEM_MIGRATIONS_FEATURE_ID } from '@kbn/security-solution-features/constants'; import { SecurityPageName, SECURITY_FEATURE_ID, @@ -24,7 +25,7 @@ export const siemMigrationsLinks: LinkItem = { }), landingIcon: SiemMigrationsIcon, path: SIEM_MIGRATIONS_RULES_PATH, - capabilities: [`${SECURITY_FEATURE_ID}.show`], + capabilities: [[`${SECURITY_FEATURE_ID}.show`, `${SIEM_MIGRATIONS_FEATURE_ID}.all`]], skipUrlState: true, hideTimeline: true, globalSearchKeywords: [ @@ -34,4 +35,9 @@ export const siemMigrationsLinks: LinkItem = { ], hideWhenExperimentalKey: 'siemMigrationsDisabled', isBeta: true, + betaOptions: { + text: i18n.translate('xpack.securitySolution.appLinks.siemMigrationsRulesTechnicalPreview', { + defaultMessage: 'Technical Preview', + }), + }, }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/api/index.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/api/index.ts index 5ebf30fda0c56..e149804630dc9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/api/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/api/index.ts @@ -25,6 +25,7 @@ import { SIEM_RULE_MIGRATION_RESOURCES_PATH, SIEM_RULE_MIGRATIONS_PREBUILT_RULES_PATH, SIEM_RULE_MIGRATIONS_INTEGRATIONS_PATH, + SIEM_RULE_MIGRATION_MISSING_PRIVILEGES_PATH, } from '../../../../common/siem_migrations/constants'; import type { CreateRuleMigrationRequestBody, @@ -42,6 +43,7 @@ import type { UpdateRuleMigrationResponse, StartRuleMigrationResponse, GetRuleMigrationIntegrationsResponse, + GetRuleMigrationPrivilegesResponse, } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; export interface GetRuleMigrationStatsParams { @@ -213,6 +215,20 @@ export const getRuleMigrations = async ({ ); }; +export interface GetRuleMigrationMissingPrivilegesParams { + /** Optional AbortSignal for cancelling request */ + signal?: AbortSignal; +} +/** Retrieves all the migration rule documents of a specific migration. */ +export const getRuleMigrationMissingPrivileges = async ({ + signal, +}: GetRuleMigrationMissingPrivilegesParams): Promise => { + return KibanaServices.get().http.get( + SIEM_RULE_MIGRATION_MISSING_PRIVILEGES_PATH, + { version: '1', signal } + ); +}; + export interface GetRuleMigrationTranslationStatsParams { /** `id` of the migration to get translation stats for */ migrationId: string; @@ -287,6 +303,8 @@ export const getIntegrations = async ({ }; export interface UpdateRulesParams { + /** `id` of the migration to install rules for */ + migrationId: string; /** The list of migration rules data to update */ rulesToUpdate: UpdateRuleMigrationData[]; /** Optional AbortSignal for cancelling request */ @@ -294,12 +312,12 @@ export interface UpdateRulesParams { } /** Updates provided migration rules. */ export const updateMigrationRules = async ({ + migrationId, rulesToUpdate, signal, }: UpdateRulesParams): Promise => { - return KibanaServices.get().http.put(SIEM_RULE_MIGRATIONS_PATH, { - version: '1', - body: JSON.stringify(rulesToUpdate), - signal, - }); + return KibanaServices.get().http.put( + replaceParams(SIEM_RULE_MIGRATION_PATH, { migration_id: migrationId }), + { version: '1', body: JSON.stringify(rulesToUpdate), signal } + ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_privileges.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_privileges.ts new file mode 100644 index 0000000000000..c837ce727f201 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_get_migration_privileges.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useCallback } from 'react'; +import { SIEM_RULE_MIGRATION_MISSING_PRIVILEGES_PATH } from '../../../../common/siem_migrations/constants'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; +import * as i18n from './translations'; +import { getRuleMigrationMissingPrivileges } from '../api'; +import { DEFAULT_QUERY_OPTIONS } from './constants'; + +export const useGetMigrationMissingPrivileges = () => { + const { addError } = useAppToasts(); + return useQuery( + ['GET', SIEM_RULE_MIGRATION_MISSING_PRIVILEGES_PATH], + async ({ signal }) => getRuleMigrationMissingPrivileges({ signal }), + { + ...DEFAULT_QUERY_OPTIONS, + onError: (error) => { + addError(error, { title: i18n.GET_MIGRATION_RULES_FAILURE }); + }, + } + ); +}; + +/** + * We should use this hook to invalidate the migration privileges cache. + * @returns A migration privileges cache invalidation callback + */ +export const useInvalidateGetMigrationPrivileges = () => { + const queryClient = useQueryClient(); + return useCallback(() => { + queryClient.invalidateQueries(['GET', SIEM_RULE_MIGRATION_MISSING_PRIVILEGES_PATH], { + refetchType: 'active', + }); + }, [queryClient]); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_update_migration_rule.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_update_migration_rule.ts index 2e603ac20b5fd..20f3d8e2ac8f7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_update_migration_rule.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/logic/use_update_migration_rule.ts @@ -11,7 +11,7 @@ import type { RuleMigration, UpdateRuleMigrationData, } from '../../../../common/siem_migrations/model/rule_migration.gen'; -import { SIEM_RULE_MIGRATIONS_PATH } from '../../../../common/siem_migrations/constants'; +import { SIEM_RULE_MIGRATION_PATH } from '../../../../common/siem_migrations/constants'; import type { UpdateRuleMigrationResponse } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; import * as i18n from './translations'; @@ -20,7 +20,7 @@ import { useInvalidateGetMigrationTranslationStats } from './use_get_migration_t import { updateMigrationRules } from '../api'; import { useTranslatedRuleTelemetry } from '../hooks/use_translated_rule_telemetry'; -export const UPDATE_MIGRATION_RULE_MUTATION_KEY = ['PUT', SIEM_RULE_MIGRATIONS_PATH]; +export const UPDATE_MIGRATION_RULE_MUTATION_KEY = ['PUT', SIEM_RULE_MIGRATION_PATH]; export const useUpdateMigrationRule = (ruleMigration: RuleMigration) => { const { addError } = useAppToasts(); @@ -44,7 +44,7 @@ export const useUpdateMigrationRule = (ruleMigration: RuleMigration) => { const invalidateGetMigrationTranslationStats = useInvalidateGetMigrationTranslationStats(); return useMutation( - (ruleUpdateData) => updateMigrationRules({ rulesToUpdate: [ruleUpdateData] }), + (ruleUpdateData) => updateMigrationRules({ migrationId, rulesToUpdate: [ruleUpdateData] }), { mutationKey: UPDATE_MIGRATION_RULE_MUTATION_KEY, onSuccess: () => reportTelemetry(), diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx index 4b3ea822c4fd6..01a414b66670b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx @@ -18,7 +18,7 @@ import { SecurityPageName } from '../../../app/types'; import { MigrationRulesTable } from '../components/rules_table'; import { NeedAdminForUpdateRulesCallOut } from '../../../detections/components/callouts/need_admin_for_update_callout'; -import { MissingPrivilegesCallOut } from '../../../detections/components/callouts/missing_privileges_callout'; +import { MissingPrivilegesCallOut } from './missing_privileges_callout'; import { HeaderButtons } from '../components/header_buttons'; import { UnknownMigration } from '../components/unknown_migration'; import { useLatestStats } from '../service/hooks/use_latest_stats'; @@ -130,30 +130,27 @@ export const MigrationRulesPage: React.FC = React.memo( }, [migrationId, refetchData, ruleMigrationsStats, integrations, isIntegrationsLoading]); return ( - <> + + + + - - - - - - - - - - } - loadedContent={content} - /> - - + + + + + } + loadedContent={content} + /> + ); } ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/pages/missing_privileges_callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/pages/missing_privileges_callout.tsx new file mode 100644 index 0000000000000..733b178dd0dfe --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/pages/missing_privileges_callout.tsx @@ -0,0 +1,72 @@ +/* + * 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, { useMemo } from 'react'; +import hash from 'object-hash'; +import { i18n } from '@kbn/i18n'; +import { DEFAULT_LISTS_INDEX, DEFAULT_ITEMS_INDEX } from '../../../../common/constants'; +import { missingPrivilegesCallOutBody } from '../../../detections/components/callouts/missing_privileges_callout/translations'; +import { + useMissingPrivileges, + type MissingPrivileges, + type MissingIndexPrivileges, +} from '../../../detections/components/callouts/missing_privileges_callout/use_missing_privileges'; +import { CallOutSwitcher, type CallOutMessage } from '../../../common/components/callouts'; +import { useGetMigrationMissingPrivileges } from '../logic/use_get_migration_privileges'; + +export const MissingPrivilegesCallOut = React.memo(() => { + const missingDetectionsPrivileges = useMissingPrivileges(); + const { data: missingIndexPrivileges = [] } = useGetMigrationMissingPrivileges(); + + const message: CallOutMessage | null = useMemo(() => { + const missingPrivileges: MissingPrivileges = { + featurePrivileges: missingDetectionsPrivileges.featurePrivileges, + indexPrivileges: [ + // include rules missing detections index privileges except of lists and items indices + ...missingDetectionsPrivileges.indexPrivileges.filter( + ([indexName]) => + !indexName.startsWith(DEFAULT_LISTS_INDEX) && !indexName.startsWith(DEFAULT_ITEMS_INDEX) + ), + // include missing siem migrations index privileges (lookups) + ...missingIndexPrivileges.map(({ indexName, privileges }) => [ + indexName, + privileges, + ]), + ], + }; + + const hasMissingPrivileges = + missingPrivileges.indexPrivileges.length > 0 || + missingPrivileges.featurePrivileges.length > 0; + + if (!hasMissingPrivileges) { + return null; + } + + const missingPrivilegesHash = hash(missingPrivileges); + return { + type: 'primary', + /** + * Use privileges hash as a part of the message id. + * We want to make sure that the user will see the + * callout message in case his privileges change. + * The previous click on Dismiss should not affect that. + */ + id: `missing-siem-migrations-privileges-${missingPrivilegesHash}`, + title: i18n.translate('xpack.securitySolution.siemMigrations.missingPrivileges.title', { + defaultMessage: 'Insufficient privileges', + }), + description: missingPrivilegesCallOutBody(missingPrivileges), + }; + }, [missingDetectionsPrivileges, missingIndexPrivileges]); + + if (!message) { + return null; + } + return ; +}); +MissingPrivilegesCallOut.displayName = 'MissingPrivilegesCallOut'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/capabilities.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/capabilities.ts new file mode 100644 index 0000000000000..fccbceb3f1646 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/capabilities.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Capabilities } from '@kbn/core/public'; +import { + SECURITY_FEATURE_ID_V2, + SIEM_MIGRATIONS_FEATURE_ID, +} from '@kbn/security-solution-features/constants'; +import { i18n } from '@kbn/i18n'; +import { CapabilitiesChecker } from '../../../common/lib/capabilities'; + +export interface MissingCapability { + capability: string; + description: string; +} + +const minimumCapabilities: MissingCapability[] = [ + { + capability: `${SECURITY_FEATURE_ID_V2}.show`, + description: i18n.translate( + 'xpack.securitySolution.siemMigrations.service.capabilities.securityAll', + { defaultMessage: 'Security > Security: Read' } + ), + }, + { + capability: `${SIEM_MIGRATIONS_FEATURE_ID}.all`, + description: i18n.translate( + 'xpack.securitySolution.siemMigrations.service.capabilities.siemMigrationsAll', + { defaultMessage: 'Security > SIEM migrations: All' } + ), + }, +]; + +const allCapabilities: MissingCapability[] = [ + { + capability: `${SECURITY_FEATURE_ID_V2}.crud`, + description: i18n.translate( + 'xpack.securitySolution.siemMigrations.service.capabilities.securityAll', + { defaultMessage: 'Security > Security: All' } + ), + }, + { + capability: `${SIEM_MIGRATIONS_FEATURE_ID}.all`, + description: i18n.translate( + 'xpack.securitySolution.siemMigrations.service.capabilities.siemMigrationsAll', + { defaultMessage: 'Security > SIEM migrations: All' } + ), + }, + { + capability: 'actions.execute', + description: i18n.translate( + 'xpack.securitySolution.siemMigrations.service.capabilities.connectorsRead', + { defaultMessage: 'Management > Actions and Connectors: Read' } + ), + }, +]; + +export type CapabilitiesLevel = 'minimum' | 'all'; + +const requiredCapabilities: Record = { + minimum: minimumCapabilities, + all: allCapabilities, +}; + +export const getMissingCapabilities = ( + capabilities: Capabilities, + level: CapabilitiesLevel = 'all' +): MissingCapability[] => { + const checker = new CapabilitiesChecker(capabilities); + return requiredCapabilities[level].filter((required) => !checker.has(required.capability)); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_start_migration.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_start_migration.ts index 9944e298a31d3..c91770077f005 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_start_migration.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_start_migration.ts @@ -32,9 +32,11 @@ export const useStartMigration = (onSuccess?: OnSuccess) => { (async () => { try { dispatch({ type: 'start' }); - await siemMigrations.rules.startRuleMigration(migrationId, retry); + const { started } = await siemMigrations.rules.startRuleMigration(migrationId, retry); - notifications.toasts.addSuccess(RULES_DATA_INPUT_START_MIGRATION_SUCCESS); + if (started) { + notifications.toasts.addSuccess(RULES_DATA_INPUT_START_MIGRATION_SUCCESS); + } dispatch({ type: 'success' }); onSuccess?.(); } catch (err) { diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/notifications/missing_capabilities_notification.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/notifications/missing_capabilities_notification.tsx new file mode 100644 index 0000000000000..e898f6483ca76 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/notifications/missing_capabilities_notification.tsx @@ -0,0 +1,52 @@ +/* + * 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 { CoreStart } from '@kbn/core-lifecycle-browser'; +import { i18n } from '@kbn/i18n'; +import type { ToastInput } from '@kbn/core-notifications-browser'; +import { toMountPoint } from '@kbn/react-kibana-mount'; +import { EuiCode, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import type { MissingCapability } from '../capabilities'; + +export const getMissingCapabilitiesToast = ( + missingCapabilities: MissingCapability[], + core: CoreStart +): ToastInput => ({ + color: 'danger', + iconType: 'alert', + title: i18n.translate( + 'xpack.securitySolution.siemMigrations.rulesService.missingCapabilities.title', + { defaultMessage: 'Insufficient privileges.' } + ), + text: toMountPoint( + + + {i18n.translate( + 'xpack.securitySolution.siemMigrations.rulesService.missingCapabilities.description', + { defaultMessage: 'The privileges required to start a rule migration are:' } + )} + + + +
    + {missingCapabilities.map(({ capability, description }) => ( +
  • {description}
  • + ))} +
+
+
+ + {i18n.translate( + 'xpack.securitySolution.siemMigrations.rulesService.missingCapabilities.contactAdministrator', + { defaultMessage: 'Contact your administrator for assistance.' } + )} + +
, + core + ), +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/notifications/no_connector_notification.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/notifications/no_connector_notification.tsx new file mode 100644 index 0000000000000..829963f6baa21 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/notifications/no_connector_notification.tsx @@ -0,0 +1,67 @@ +/* + * 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 { CoreStart } from '@kbn/core-lifecycle-browser'; +import { i18n } from '@kbn/i18n'; +import { + SecurityPageName, + useNavigation, + NavigationProvider, +} from '@kbn/security-solution-navigation'; +import type { ToastInput } from '@kbn/core-notifications-browser'; +import { toMountPoint } from '@kbn/react-kibana-mount'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiLink, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { OnboardingCardId, OnboardingTopicId } from '../../../../onboarding/constants'; + +export const getNoConnectorToast = (core: CoreStart): ToastInput => ({ + color: 'danger', + iconType: 'alert', + title: i18n.translate('xpack.securitySolution.siemMigrations.rulesService.noConnector.title', { + defaultMessage: 'No connector configured.', + }), + text: toMountPoint( + + + , + core + ), +}); + +const navigation = { + deepLinkId: SecurityPageName.landing, + path: `${OnboardingTopicId.siemMigrations}#${OnboardingCardId.siemMigrationsAiConnectors}`, +}; + +const NoConnectorToastContent: React.FC = () => { + const { navigateTo, getAppUrl } = useNavigation(); + const onClick: React.MouseEventHandler = (ev) => { + ev.preventDefault(); + navigateTo(navigation); + }; + const url = getAppUrl(navigation); + + return ( + + + + + + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + + {i18n.translate('xpack.securitySolution.siemMigrations.rulesService.noConnector.link', { + defaultMessage: 'Go to connector selection', + })} + + + + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/success_notification.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/notifications/success_notification.tsx similarity index 97% rename from x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/success_notification.tsx rename to x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/notifications/success_notification.tsx index f87755943f830..f1a59a045b4fc 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/success_notification.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/notifications/success_notification.tsx @@ -17,7 +17,7 @@ import type { ToastInput } from '@kbn/core-notifications-browser'; import { toMountPoint } from '@kbn/react-kibana-mount'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import type { RuleMigrationStats } from '../types'; +import type { RuleMigrationStats } from '../../types'; export const getSuccessToast = (migration: RuleMigrationStats, core: CoreStart): ToastInput => ({ color: 'success', diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts index 07ef1971870b9..54933ad4bc3a5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts @@ -39,10 +39,17 @@ import { upsertMigrationResources, getIntegrations, } from '../api'; +import { + getMissingCapabilities, + type MissingCapability, + type CapabilitiesLevel, +} from './capabilities'; import type { RuleMigrationStats } from '../types'; -import { getSuccessToast } from './success_notification'; +import { getSuccessToast } from './notifications/success_notification'; import { RuleMigrationsStorage } from './storage'; import * as i18n from './translations'; +import { getNoConnectorToast } from './notifications/no_connector_notification'; +import { getMissingCapabilitiesToast } from './notifications/missing_capabilities_notification'; // use the default assistant namespace since it's the only one we use const NAMESPACE_TRACE_OPTIONS_SESSION_STORAGE_KEY = @@ -76,9 +83,19 @@ export class SiemRulesMigrationsService { return this.latestStats$.asObservable(); } + public getMissingCapabilities(level?: CapabilitiesLevel): MissingCapability[] { + return getMissingCapabilities(this.core.application.capabilities, level); + } + + public hasMissingCapabilities(level?: CapabilitiesLevel): boolean { + return this.getMissingCapabilities(level).length > 0; + } + public isAvailable() { return ( - !ExperimentalFeaturesService.get().siemMigrationsDisabled && licenseService.isEnterprise() + !ExperimentalFeaturesService.get().siemMigrationsDisabled && + licenseService.isEnterprise() && + !this.hasMissingCapabilities('minimum') ); } @@ -128,9 +145,17 @@ export class SiemRulesMigrationsService { migrationId: string, retry?: SiemMigrationRetryFilter ): Promise { + const missingCapabilities = this.getMissingCapabilities('all'); + if (missingCapabilities.length > 0) { + this.core.notifications.toasts.add( + getMissingCapabilitiesToast(missingCapabilities, this.core) + ); + return { started: false }; + } const connectorId = this.connectorIdStorage.get(); if (!connectorId) { - throw new Error(i18n.MISSING_CONNECTOR_ERROR); + this.core.notifications.toasts.add(getNoConnectorToast(this.core)); + return { started: false }; } const langSmithSettings = this.traceOptionsStorage.get(); @@ -176,8 +201,9 @@ export class SiemRulesMigrationsService { } return getRuleMigrationsStatsAll(params).catch((e) => { - // Retry only on network errors (no status) and 503s, otherwise throw - if (e.status && e.status !== 503) { + // Retry only on network errors (no status) and 503 (Service Unavailable), otherwise throw + const status = e.response?.status || e.status; + if (status && status !== 503) { throw e; } const nextSleepSecs = sleepSecs ? sleepSecs * 2 : 1; // Exponential backoff @@ -217,7 +243,7 @@ export class SiemRulesMigrationsService { if (result.status === SiemMigrationTaskStatus.STOPPED) { const connectorId = this.connectorIdStorage.get(); - if (connectorId) { + if (connectorId && !this.hasMissingCapabilities('all')) { // automatically resume stopped migrations when connector is available await startRuleMigration({ migrationId: result.id, connectorId }); pendingMigrationIds.push(result.id); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/translations.ts index 41a897a56e9df..20362e73db783 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/translations.ts @@ -12,11 +12,6 @@ export const POLLING_ERROR = i18n.translate( { defaultMessage: 'Error fetching rule migrations' } ); -export const MISSING_CONNECTOR_ERROR = i18n.translate( - 'xpack.securitySolution.siemMigrations.rulesService.create.missingConnectorError', - { defaultMessage: 'Connector not defined. Please set a connector ID first.' } -); - export const EMPTY_RULES_ERROR = i18n.translate( 'xpack.securitySolution.siemMigrations.rulesService.create.emptyRulesError', { defaultMessage: 'Can not create a migration without rules' } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/mocks.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/mocks.ts index 7d0001c4d9ac7..9122bc202e73b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/mocks.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/mocks.ts @@ -61,6 +61,11 @@ jest.mock('@kbn/security-solution-features/product_features', () => ({ baseKibanaSubFeatureIds: [], subFeaturesMap: new Map(), })), + getSiemMigrationsFeature: jest.fn(() => ({ + baseKibanaFeature: {}, + baseKibanaSubFeatureIds: [], + subFeaturesMap: new Map(), + })), })); export const createProductFeaturesServiceMock = ( @@ -186,6 +191,21 @@ export const createProductFeaturesServiceMock = ( ]) ) ), + siemMigrations: jest.fn().mockReturnValue( + new Map( + enabledFeatureKeys.map((key) => [ + key, + { + privileges: { + all: { + api: ['test-api-action'], + ui: ['test-ui-action'], + }, + }, + }, + ]) + ) + ), }); } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_service.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_service.test.ts index 5d9a028e10eab..a23673071ea67 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_service.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_service.test.ts @@ -10,11 +10,11 @@ import { ProductFeatures } from './product_features'; import type { ProductFeaturesConfig, BaseKibanaFeatureConfig, + ProductFeaturesConfigurator, } from '@kbn/security-solution-features'; import { loggerMock } from '@kbn/logging-mocks'; import type { ExperimentalFeatures } from '../../../common'; import { featuresPluginMock } from '@kbn/features-plugin/server/mocks'; -import type { ProductFeaturesConfigurator } from './types'; import type { AssistantSubFeatureId, CasesSubFeatureId, @@ -41,15 +41,16 @@ const productFeature = { }; const mockGetFeature = jest.fn().mockReturnValue(productFeature); jest.mock('@kbn/security-solution-features/product_features', () => ({ - getAttackDiscoveryFeature: () => mockGetFeature(), - getAssistantFeature: () => mockGetFeature(), + getSecurityFeature: () => mockGetFeature(), + getSecurityV2Feature: () => mockGetFeature(), getCasesFeature: () => mockGetFeature(), getCasesV2Feature: () => mockGetFeature(), getCasesV3Feature: () => mockGetFeature(), - getSecurityFeature: () => mockGetFeature(), - getSecurityV2Feature: () => mockGetFeature(), + getAttackDiscoveryFeature: () => mockGetFeature(), + getAssistantFeature: () => mockGetFeature(), getTimelineFeature: () => mockGetFeature(), getNotesFeature: () => mockGetFeature(), + getSiemMigrationsFeature: () => mockGetFeature(), })); describe('ProductFeaturesService', () => { @@ -61,8 +62,8 @@ describe('ProductFeaturesService', () => { const experimentalFeatures = {} as ExperimentalFeatures; new ProductFeaturesService(loggerMock.create(), experimentalFeatures); - expect(mockGetFeature).toHaveBeenCalledTimes(9); - expect(MockedProductFeatures).toHaveBeenCalledTimes(9); + expect(mockGetFeature).toHaveBeenCalledTimes(10); + expect(MockedProductFeatures).toHaveBeenCalledTimes(10); }); it('should init all ProductFeatures when initialized', () => { @@ -94,14 +95,16 @@ describe('ProductFeaturesService', () => { const mockCasesConfig = new Map() as ProductFeaturesConfig; const mockAssistantConfig = new Map() as ProductFeaturesConfig; const mockAttackDiscoveryConfig = new Map() as ProductFeaturesConfig; + const mockSiemMigrationsConfig = new Map() as ProductFeaturesConfig; const mockTimelineConfig = new Map() as ProductFeaturesConfig; const mockNotesConfig = new Map() as ProductFeaturesConfig; const configurator: ProductFeaturesConfigurator = { - attackDiscovery: jest.fn(() => mockAttackDiscoveryConfig), security: jest.fn(() => mockSecurityConfig), cases: jest.fn(() => mockCasesConfig), securityAssistant: jest.fn(() => mockAssistantConfig), + attackDiscovery: jest.fn(() => mockAttackDiscoveryConfig), + siemMigrations: jest.fn(() => mockSiemMigrationsConfig), timeline: jest.fn(() => mockTimelineConfig), notes: jest.fn(() => mockNotesConfig), }; @@ -111,6 +114,7 @@ describe('ProductFeaturesService', () => { expect(configurator.cases).toHaveBeenCalled(); expect(configurator.securityAssistant).toHaveBeenCalled(); expect(configurator.attackDiscovery).toHaveBeenCalled(); + expect(configurator.siemMigrations).toHaveBeenCalled(); expect(MockedProductFeatures.mock.instances[0].setConfig).toHaveBeenCalledWith( mockSecurityConfig @@ -122,6 +126,9 @@ describe('ProductFeaturesService', () => { expect(MockedProductFeatures.mock.instances[3].setConfig).toHaveBeenCalledWith( mockAttackDiscoveryConfig ); + expect(MockedProductFeatures.mock.instances[3].setConfig).toHaveBeenCalledWith( + mockSiemMigrationsConfig + ); }); it('should return isEnabled for enabled features', () => { @@ -147,14 +154,18 @@ describe('ProductFeaturesService', () => { const mockAttackDiscoveryConfig = new Map([ [ProductFeatureKey.attackDiscovery, {}], ]) as ProductFeaturesConfig; + const mockSiemMigrationsConfig = new Map([ + [ProductFeatureKey.siemMigrations, {}], + ]) as ProductFeaturesConfig; const mockTimelineConfig = new Map([[ProductFeatureKey.timeline, {}]]) as ProductFeaturesConfig; const mockNotesConfig = new Map([[ProductFeatureKey.notes, {}]]) as ProductFeaturesConfig; const configurator: ProductFeaturesConfigurator = { - attackDiscovery: jest.fn(() => mockAttackDiscoveryConfig), security: jest.fn(() => mockSecurityConfig), cases: jest.fn(() => mockCasesConfig), securityAssistant: jest.fn(() => mockAssistantConfig), + attackDiscovery: jest.fn(() => mockAttackDiscoveryConfig), + siemMigrations: jest.fn(() => mockSiemMigrationsConfig), timeline: jest.fn(() => mockTimelineConfig), notes: jest.fn(() => mockNotesConfig), }; @@ -165,6 +176,7 @@ describe('ProductFeaturesService', () => { expect(productFeaturesService.isEnabled(ProductFeatureKey.casesConnectors)).toEqual(true); expect(productFeaturesService.isEnabled(ProductFeatureKey.assistant)).toEqual(true); expect(productFeaturesService.isEnabled(ProductFeatureKey.attackDiscovery)).toEqual(true); + expect(productFeaturesService.isEnabled(ProductFeatureKey.siemMigrations)).toEqual(true); expect(productFeaturesService.isEnabled(ProductFeatureKey.externalRuleActions)).toEqual(false); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_service.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_service.ts index 2c2f96face5d3..72d8e8ed3900a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_service.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_service.ts @@ -4,17 +4,14 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.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 type { AuthzEnabled, HttpServiceSetup, Logger, RouteAuthz } from '@kbn/core/server'; import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects'; import type { FeaturesPluginSetup } from '@kbn/features-plugin/server'; -import type { ProductFeatureKeyType } from '@kbn/security-solution-features'; +import type { + ProductFeatureKeyType, + ProductFeaturesConfigurator, +} from '@kbn/security-solution-features'; import { getAssistantFeature, getAttackDiscoveryFeature, @@ -25,12 +22,13 @@ import { getSecurityV2Feature, getTimelineFeature, getNotesFeature, + getSiemMigrationsFeature, } from '@kbn/security-solution-features/product_features'; +import { API_ACTION_PREFIX } from '@kbn/security-solution-features/actions'; import type { RecursiveReadonly } from '@kbn/utility-types'; import type { ExperimentalFeatures } from '../../../common'; import { APP_ID } from '../../../common'; import { ProductFeatures } from './product_features'; -import type { ProductFeaturesConfigurator } from './types'; import { securityDefaultSavedObjects, securityNotesSavedObjects, @@ -39,9 +37,6 @@ import { } from './security_saved_objects'; import { casesApiTags, casesUiCapabilities } from './cases_privileges'; -// The prefix ("securitySolution-") used by all the Security Solution API action privileges. -export const API_ACTION_PREFIX = `${APP_ID}-`; - export class ProductFeaturesService { private securityProductFeatures: ProductFeatures; private securityV2ProductFeatures: ProductFeatures; @@ -52,6 +47,8 @@ export class ProductFeaturesService { private attackDiscoveryProductFeatures: ProductFeatures; private timelineProductFeatures: ProductFeatures; private notesProductFeatures: ProductFeatures; + private siemMigrationsProductFeatures: ProductFeatures; + private productFeatures?: Set; constructor( @@ -155,6 +152,14 @@ export class ProductFeaturesService { notesFeature.baseKibanaFeature, notesFeature.baseKibanaSubFeatureIds ); + + const siemMigrationsFeature = getSiemMigrationsFeature(); + this.siemMigrationsProductFeatures = new ProductFeatures( + this.logger, + siemMigrationsFeature.subFeaturesMap, + siemMigrationsFeature.baseKibanaFeature, + siemMigrationsFeature.baseKibanaSubFeatureIds + ); } public init(featuresSetup: FeaturesPluginSetup) { @@ -167,6 +172,7 @@ export class ProductFeaturesService { this.attackDiscoveryProductFeatures.init(featuresSetup); this.timelineProductFeatures.init(featuresSetup); this.notesProductFeatures.init(featuresSetup); + this.siemMigrationsProductFeatures.init(featuresSetup); } public setProductFeaturesConfigurator(configurator: ProductFeaturesConfigurator) { @@ -191,6 +197,12 @@ export class ProductFeaturesService { const notesProductFeaturesConfig = configurator.notes(); this.notesProductFeatures.setConfig(notesProductFeaturesConfig); + let siemMigrationsProductFeaturesConfig = new Map(); + if (!this.experimentalFeatures.siemMigrationsDisabled) { + siemMigrationsProductFeaturesConfig = configurator.siemMigrations(); + this.siemMigrationsProductFeatures.setConfig(siemMigrationsProductFeaturesConfig); + } + this.productFeatures = new Set( Object.freeze([ ...securityProductFeaturesConfig.keys(), @@ -199,6 +211,7 @@ export class ProductFeaturesService { ...attackDiscoveryProductFeaturesConfig.keys(), ...timelineProductFeaturesConfig.keys(), ...notesProductFeaturesConfig.keys(), + ...siemMigrationsProductFeaturesConfig.keys(), ]) as readonly ProductFeatureKeyType[] ); } @@ -219,7 +232,8 @@ export class ProductFeaturesService { this.securityAssistantProductFeatures.isActionRegistered(action) || this.attackDiscoveryProductFeatures.isActionRegistered(action) || this.timelineProductFeatures.isActionRegistered(action) || - this.notesProductFeatures.isActionRegistered(action) + this.notesProductFeatures.isActionRegistered(action) || + this.siemMigrationsProductFeatures.isActionRegistered(action) ); } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/types.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/types.ts deleted file mode 100644 index 4bc7956c7861f..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/types.ts +++ /dev/null @@ -1,22 +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 type { ProductFeaturesConfig } from '@kbn/security-solution-features'; -import type { - SecuritySubFeatureId, - CasesSubFeatureId, - AssistantSubFeatureId, -} from '@kbn/security-solution-features/keys'; - -export interface ProductFeaturesConfigurator { - attackDiscovery: () => ProductFeaturesConfig; - security: () => ProductFeaturesConfig; - cases: () => ProductFeaturesConfig; - securityAssistant: () => ProductFeaturesConfig; - timeline: () => ProductFeaturesConfig; - notes: () => ProductFeaturesConfig; -} diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/create.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/create.ts index fd18085aa2f80..81088725a6e54 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/create.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/create.ts @@ -17,7 +17,8 @@ import { import { ResourceIdentifier } from '../../../../../common/siem_migrations/rules/resources'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import type { CreateRuleMigrationInput } from '../data/rule_migrations_data_rules_client'; -import { SiemMigrationAuditLogger, SiemMigrationsAuditActions } from './util/audit'; +import { SiemMigrationAuditLogger } from './util/audit'; +import { authz } from './util/authz'; import { withLicense } from './util/with_license'; export const registerSiemRuleMigrationsCreateRoute = ( @@ -28,7 +29,7 @@ export const registerSiemRuleMigrationsCreateRoute = ( .post({ path: SIEM_RULE_MIGRATION_CREATE_PATH, access: 'internal', - security: { authz: { requiredPrivileges: ['securitySolution'] } }, + security: { authz }, }) .addVersion( { @@ -44,23 +45,17 @@ export const registerSiemRuleMigrationsCreateRoute = ( async (context, req, res): Promise> => { const originalRules = req.body; const migrationId = req.params.migration_id ?? uuidV4(); - let siemMigrationAuditLogger: SiemMigrationAuditLogger | undefined; + const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution); try { const [firstOriginalRule] = originalRules; if (!firstOriginalRule) { return res.noContent(); } - const ctx = await context.resolve(['securitySolution']); const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); - const auditLogger = ctx.securitySolution.getAuditLogger(); - if (auditLogger) { - siemMigrationAuditLogger = new SiemMigrationAuditLogger(auditLogger); - } - siemMigrationAuditLogger?.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_CREATED, - id: migrationId, - }); + + await siemMigrationAuditLogger.logCreateMigration({ migrationId }); + const ruleMigrations = originalRules.map((originalRule) => ({ migration_id: migrationId, original_rule: originalRule, @@ -79,14 +74,10 @@ export const registerSiemRuleMigrationsCreateRoute = ( } return res.ok({ body: { migration_id: migrationId } }); - } catch (err) { - logger.error(err); - siemMigrationAuditLogger?.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_CREATED, - error: err, - id: migrationId, - }); - return res.badRequest({ body: err.message }); + } catch (error) { + logger.error(error); + await siemMigrationAuditLogger.logCreateMigration({ migrationId, error }); + return res.badRequest({ body: error.message }); } } ) diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get.ts index bf5dd650475db..5fa24810eafcb 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get.ts @@ -15,7 +15,8 @@ import { } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import type { RuleMigrationGetOptions } from '../data/rule_migrations_data_rules_client'; -import { SiemMigrationAuditLogger, SiemMigrationsAuditActions } from './util/audit'; +import { SiemMigrationAuditLogger } from './util/audit'; +import { authz } from './util/authz'; import { withLicense } from './util/with_license'; export const registerSiemRuleMigrationsGetRoute = ( @@ -26,7 +27,7 @@ export const registerSiemRuleMigrationsGetRoute = ( .get({ path: SIEM_RULE_MIGRATION_PATH, access: 'internal', - security: { authz: { requiredPrivileges: ['securitySolution'] } }, + security: { authz }, }) .addVersion( { @@ -40,60 +41,37 @@ export const registerSiemRuleMigrationsGetRoute = ( }, withLicense(async (context, req, res): Promise> => { const { migration_id: migrationId } = req.params; - const { - page, - per_page: perPage, - sort_field: sortField, - sort_direction: sortDirection, - search_term: searchTerm, - ids, - is_prebuilt: isPrebuilt, - is_installed: isInstalled, - is_fully_translated: isFullyTranslated, - is_partially_translated: isPartiallyTranslated, - is_untranslatable: isUntranslatable, - is_failed: isFailed, - } = req.query; - let siemMigrationAuditLogger: SiemMigrationAuditLogger | undefined; + + const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution); try { const ctx = await context.resolve(['securitySolution']); - const auditLogger = ctx.securitySolution.getAuditLogger(); - if (auditLogger) { - siemMigrationAuditLogger = new SiemMigrationAuditLogger(auditLogger); - } const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); + const { page, per_page: size } = req.query; const options: RuleMigrationGetOptions = { filters: { - searchTerm, - ids, - prebuilt: isPrebuilt, - installed: isInstalled, - fullyTranslated: isFullyTranslated, - partiallyTranslated: isPartiallyTranslated, - untranslatable: isUntranslatable, - failed: isFailed, + searchTerm: req.query.search_term, + ids: req.query.ids, + prebuilt: req.query.is_prebuilt, + installed: req.query.is_installed, + fullyTranslated: req.query.is_fully_translated, + partiallyTranslated: req.query.is_partially_translated, + untranslatable: req.query.is_untranslatable, + failed: req.query.is_failed, }, - sort: { sortField, sortDirection }, - size: perPage, - from: page && perPage ? page * perPage : 0, + sort: { sortField: req.query.sort_field, sortDirection: req.query.sort_direction }, + size, + from: page && size ? page * size : 0, }; const result = await ruleMigrationsClient.data.rules.get(migrationId, options); - siemMigrationAuditLogger?.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_RETRIEVED, - id: migrationId, - }); + await siemMigrationAuditLogger.logGetMigration({ migrationId }); return res.ok({ body: result }); - } catch (err) { - logger.error(err); - siemMigrationAuditLogger?.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_RETRIEVED, - error: err, - id: migrationId, - }); - return res.badRequest({ body: err.message }); + } catch (error) { + logger.error(error); + await siemMigrationAuditLogger.logGetMigration({ migrationId, error }); + return res.badRequest({ body: error.message }); } }) ); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get_integrations.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get_integrations.ts index afc7c7b9608d3..7e3385b7f26ec 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get_integrations.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get_integrations.ts @@ -10,6 +10,7 @@ import type { RelatedIntegration } from '../../../../../common/api/detection_eng import { type GetRuleMigrationIntegrationsResponse } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import { SIEM_RULE_MIGRATIONS_INTEGRATIONS_PATH } from '../../../../../common/siem_migrations/constants'; import type { SecuritySolutionPluginRouter } from '../../../../types'; +import { authz } from './util/authz'; import { withLicense } from './util/with_license'; export const registerSiemRuleMigrationsIntegrationsRoute = ( @@ -20,7 +21,7 @@ export const registerSiemRuleMigrationsIntegrationsRoute = ( .get({ path: SIEM_RULE_MIGRATIONS_INTEGRATIONS_PATH, access: 'internal', - security: { authz: { requiredPrivileges: ['securitySolution'] } }, + security: { authz }, }) .addVersion( { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get_prebuilt_rules.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get_prebuilt_rules.ts index 8165b858e2a31..429db0012adc4 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get_prebuilt_rules.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/get_prebuilt_rules.ts @@ -11,6 +11,7 @@ import type { GetRuleMigrationPrebuiltRulesResponse } from '../../../../../commo import { GetRuleMigrationPrebuiltRulesRequestParams } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import { SIEM_RULE_MIGRATIONS_PREBUILT_RULES_PATH } from '../../../../../common/siem_migrations/constants'; import type { SecuritySolutionPluginRouter } from '../../../../types'; +import { authz } from './util/authz'; import { withLicense } from './util/with_license'; import { getPrebuiltRulesForMigration } from './util/prebuilt_rules'; @@ -22,7 +23,7 @@ export const registerSiemRuleMigrationsPrebuiltRulesRoute = ( .get({ path: SIEM_RULE_MIGRATIONS_PREBUILT_RULES_PATH, access: 'internal', - security: { authz: { requiredPrivileges: ['securitySolution'] } }, + security: { authz }, }) .addVersion( { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts index 5f2049fd8fe12..8f97d91a785c0 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts @@ -21,6 +21,7 @@ import { registerSiemRuleMigrationsInstallRoute } from './install'; import { registerSiemRuleMigrationsResourceGetMissingRoute } from './resources/missing'; import { registerSiemRuleMigrationsPrebuiltRulesRoute } from './get_prebuilt_rules'; import { registerSiemRuleMigrationsIntegrationsRoute } from './get_integrations'; +import { registerSiemRuleMigrationsGetMissingPrivilegesRoute } from './privileges/get_missing_privileges'; export const registerSiemRuleMigrationsRoutes = ( router: SecuritySolutionPluginRouter, @@ -41,4 +42,6 @@ export const registerSiemRuleMigrationsRoutes = ( registerSiemRuleMigrationsResourceUpsertRoute(router, logger); registerSiemRuleMigrationsResourceGetRoute(router, logger); registerSiemRuleMigrationsResourceGetMissingRoute(router, logger); + + registerSiemRuleMigrationsGetMissingPrivilegesRoute(router, logger); }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/install.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/install.ts index 1ba67fbdf3e40..9d3d13d6d0f6f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/install.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/install.ts @@ -14,8 +14,9 @@ import { InstallMigrationRulesRequestParams, } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import type { SecuritySolutionPluginRouter } from '../../../../types'; -import { SiemMigrationAuditLogger, SiemMigrationsAuditActions } from './util/audit'; +import { SiemMigrationAuditLogger } from './util/audit'; import { installTranslated } from './util/installation'; +import { authz } from './util/authz'; import { withLicense } from './util/with_license'; export const registerSiemRuleMigrationsInstallRoute = ( @@ -26,7 +27,7 @@ export const registerSiemRuleMigrationsInstallRoute = ( .post({ path: SIEM_RULE_MIGRATION_INSTALL_PATH, access: 'internal', - security: { authz: { requiredPrivileges: ['securitySolution'] } }, + security: { authz }, }) .addVersion( { @@ -42,7 +43,7 @@ export const registerSiemRuleMigrationsInstallRoute = ( async (context, req, res): Promise> => { const { migration_id: migrationId } = req.params; const { ids, enabled = false } = req.body; - let siemMigrationAuditLogger: SiemMigrationAuditLogger | undefined; + const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution); try { const ctx = await context.resolve(['core', 'alerting', 'securitySolution']); @@ -50,14 +51,9 @@ export const registerSiemRuleMigrationsInstallRoute = ( const securitySolutionContext = ctx.securitySolution; const savedObjectsClient = ctx.core.savedObjects.client; const rulesClient = await ctx.alerting.getRulesClient(); - const auditLogger = ctx.securitySolution.getAuditLogger(); - if (auditLogger) { - siemMigrationAuditLogger = new SiemMigrationAuditLogger(auditLogger); - } - siemMigrationAuditLogger?.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_INSTALLED_RULE, - id: migrationId, - }); + + await siemMigrationAuditLogger.logInstallRules({ ids, migrationId }); + const installed = await installTranslated({ migrationId, ids, @@ -68,14 +64,10 @@ export const registerSiemRuleMigrationsInstallRoute = ( }); return res.ok({ body: { installed } }); - } catch (err) { - logger.error(err); - siemMigrationAuditLogger?.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_INSTALLED_RULE, - error: err, - id: migrationId, - }); - return res.badRequest({ body: err.message }); + } catch (error) { + logger.error(error); + await siemMigrationAuditLogger.logInstallRules({ ids, migrationId, error }); + return res.badRequest({ body: error.message }); } } ) diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/privileges/get_missing_privileges.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/privileges/get_missing_privileges.ts new file mode 100644 index 0000000000000..8d392deadd930 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/privileges/get_missing_privileges.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ElasticsearchClient, IKibanaResponse, Logger } from '@kbn/core/server'; +import type { SecurityHasPrivilegesResponse } from '@elastic/elasticsearch/lib/api/types'; +import type { GetRuleMigrationPrivilegesResponse } from '../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import { + SIEM_RULE_MIGRATION_MISSING_PRIVILEGES_PATH, + LOOKUPS_INDEX_PREFIX, +} from '../../../../../../common/siem_migrations/constants'; +import type { SecuritySolutionPluginRouter } from '../../../../../types'; +import { authz } from '../util/authz'; +import { withLicense } from '../util/with_license'; + +export const registerSiemRuleMigrationsGetMissingPrivilegesRoute = ( + router: SecuritySolutionPluginRouter, + logger: Logger +) => { + router.versioned + .get({ + path: SIEM_RULE_MIGRATION_MISSING_PRIVILEGES_PATH, + access: 'internal', + security: { authz }, + }) + .addVersion( + { version: '1', validate: false }, + withLicense( + async ( + context, + request, + response + ): Promise> => { + try { + const core = await context.core; + const securitySolution = await context.securitySolution; + const siemClient = securitySolution?.getAppClient(); + const esClient = core.elasticsearch.client.asCurrentUser; + + if (!siemClient) { + return response.notFound(); + } + + const lookupsIndexPattern = `${LOOKUPS_INDEX_PREFIX}*`; + const privileges = await readIndexPrivileges(esClient, lookupsIndexPattern); + + const missingPrivileges = []; + if (!privileges.index[lookupsIndexPattern].read) { + missingPrivileges.push({ indexName: lookupsIndexPattern, privileges: ['read'] }); + } + + return response.ok({ body: missingPrivileges }); + } catch (err) { + logger.error(err); + return response.badRequest({ body: err.message }); + } + } + ) + ); +}; + +const readIndexPrivileges = async ( + esClient: ElasticsearchClient, + index: string +): Promise => { + const response = await esClient.security.hasPrivileges( + { + body: { + index: [{ names: [index], privileges: ['read', 'write', 'manage', 'create_index'] }], + }, + }, + { meta: true } + ); + return response.body; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/get.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/get.ts index 994438c548b78..c35d68e51d094 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/get.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/get.ts @@ -14,7 +14,8 @@ import { type GetRuleMigrationResourcesResponse, } from '../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; -import { SiemMigrationAuditLogger, SiemMigrationsAuditActions } from '../util/audit'; +import { SiemMigrationAuditLogger } from '../util/audit'; +import { authz } from '../util/authz'; import { withLicense } from '../util/with_license'; export const registerSiemRuleMigrationsResourceGetRoute = ( @@ -25,7 +26,7 @@ export const registerSiemRuleMigrationsResourceGetRoute = ( .get({ path: SIEM_RULE_MIGRATION_RESOURCES_PATH, access: 'internal', - security: { authz: { requiredPrivileges: ['securitySolution'] } }, + security: { authz }, }) .addVersion( { @@ -41,48 +42,21 @@ export const registerSiemRuleMigrationsResourceGetRoute = ( async (context, req, res): Promise> => { const migrationId = req.params.migration_id; const { type, names, from, size } = req.query; - let siemMigrationAuditLogger: SiemMigrationAuditLogger | undefined; + const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution); try { const ctx = await context.resolve(['securitySolution']); const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); - const auditLogger = ctx.securitySolution.getAuditLogger(); - if (auditLogger) { - siemMigrationAuditLogger = new SiemMigrationAuditLogger(auditLogger); - } + const options = { filters: { type, names }, from, size }; const resources = await ruleMigrationsClient.data.resources.get(migrationId, options); - if (type && type === 'macro') { - siemMigrationAuditLogger?.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_RETRIEVED_MACRO, - id: migrationId, - }); - } - if (type && type === 'lookup') { - siemMigrationAuditLogger?.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_RETRIEVED_LOOKUP, - id: migrationId, - }); - } + await siemMigrationAuditLogger.logGetResources({ migrationId }); return res.ok({ body: resources }); - } catch (err) { - logger.error(err); - if (type && type === 'macro') { - siemMigrationAuditLogger?.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_RETRIEVED_MACRO, - error: err, - id: migrationId, - }); - } - if (type && type === 'lookup') { - siemMigrationAuditLogger?.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_RETRIEVED_LOOKUP, - error: err, - id: migrationId, - }); - } - return res.badRequest({ body: err.message }); + } catch (error) { + logger.error(error); + await siemMigrationAuditLogger.logGetResources({ migrationId, error }); + return res.badRequest({ body: error.message }); } } ) diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/missing.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/missing.ts index 6b2108703bcc6..d1ef064373133 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/missing.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/missing.ts @@ -14,6 +14,7 @@ import { } from '../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import { SIEM_RULE_MIGRATION_RESOURCES_MISSING_PATH } from '../../../../../../common/siem_migrations/constants'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; +import { authz } from '../util/authz'; import { withLicense } from '../util/with_license'; export const registerSiemRuleMigrationsResourceGetMissingRoute = ( @@ -24,7 +25,7 @@ export const registerSiemRuleMigrationsResourceGetMissingRoute = ( .get({ path: SIEM_RULE_MIGRATION_RESOURCES_MISSING_PATH, access: 'internal', - security: { authz: { requiredPrivileges: ['securitySolution'] } }, + security: { authz }, }) .addVersion( { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/upsert.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/upsert.ts index f1b09676e5f35..a2f6fce5d67ce 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/upsert.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/upsert.ts @@ -17,7 +17,8 @@ import { import { ResourceIdentifier } from '../../../../../../common/siem_migrations/rules/resources'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; import type { CreateRuleMigrationResourceInput } from '../../data/rule_migrations_data_resources_client'; -import { SiemMigrationAuditLogger, SiemMigrationsAuditActions } from '../util/audit'; +import { SiemMigrationAuditLogger } from '../util/audit'; +import { authz } from '../util/authz'; import { processLookups } from '../util/lookups'; import { withLicense } from '../util/with_license'; @@ -29,7 +30,7 @@ export const registerSiemRuleMigrationsResourceUpsertRoute = ( .post({ path: SIEM_RULE_MIGRATION_RESOURCES_PATH, access: 'internal', - security: { authz: { requiredPrivileges: ['securitySolution'] } }, + security: { authz }, options: { body: { maxBytes: 26214400 } }, // rise payload limit to 25MB }) .addVersion( @@ -50,29 +51,12 @@ export const registerSiemRuleMigrationsResourceUpsertRoute = ( ): Promise> => { const resources = req.body; const migrationId = req.params.migration_id; - let siemMigrationAuditLogger: SiemMigrationAuditLogger | undefined; + const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution); try { const ctx = await context.resolve(['securitySolution']); const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); - const auditLogger = ctx.securitySolution.getAuditLogger(); - if (auditLogger) { - siemMigrationAuditLogger = new SiemMigrationAuditLogger(auditLogger); - } - for (const resource of resources) { - if (resource.type === 'macro') { - siemMigrationAuditLogger?.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_UPLOADED_MACRO, - id: migrationId, - }); - } - if (resource.type === 'lookup') { - siemMigrationAuditLogger?.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_UPLOADED_LOOKUP, - id: migrationId, - }); - } - } + await siemMigrationAuditLogger.logUploadResources({ migrationId }); // Check if the migration exists const { data } = await ruleMigrationsClient.data.rules.get(migrationId, { size: 1 }); @@ -102,25 +86,10 @@ export const registerSiemRuleMigrationsResourceUpsertRoute = ( await ruleMigrationsClient.data.resources.create(resourcesToCreate); return res.ok({ body: { acknowledged: true } }); - } catch (err) { - logger.error(err); - for (const resource of resources) { - if (resource.type === 'macro') { - siemMigrationAuditLogger?.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_UPLOADED_MACRO, - error: err, - id: migrationId, - }); - } - if (resource.type === 'lookup') { - siemMigrationAuditLogger?.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_UPLOADED_LOOKUP, - error: err, - id: migrationId, - }); - } - } - return res.badRequest({ body: err.message }); + } catch (error) { + logger.error(error); + await siemMigrationAuditLogger.logUploadResources({ migrationId, error }); + return res.badRequest({ body: error.message }); } } ) diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/start.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/start.ts index 8a7823d31cbdd..90b1352d48b4e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/start.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/start.ts @@ -18,7 +18,8 @@ import { type StartRuleMigrationResponse, } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import type { SecuritySolutionPluginRouter } from '../../../../types'; -import { SiemMigrationAuditLogger, SiemMigrationsAuditActions } from './util/audit'; +import { SiemMigrationAuditLogger } from './util/audit'; +import { authz } from './util/authz'; import { getRetryFilter } from './util/retry'; import { withLicense } from './util/with_license'; @@ -30,7 +31,7 @@ export const registerSiemRuleMigrationsStartRoute = ( .put({ path: SIEM_RULE_MIGRATION_START_PATH, access: 'internal', - security: { authz: { requiredPrivileges: ['securitySolution'] } }, + security: { authz }, }) .addVersion( { @@ -50,15 +51,18 @@ export const registerSiemRuleMigrationsStartRoute = ( connector_id: connectorId, retry, } = req.body; - let siemMigrationAuditLogger: SiemMigrationAuditLogger | undefined; + + const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution); try { const ctx = await context.resolve(['core', 'actions', 'alerting', 'securitySolution']); - const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); - const auditLogger = ctx.securitySolution.getAuditLogger(); - if (auditLogger) { - siemMigrationAuditLogger = new SiemMigrationAuditLogger(auditLogger); + // Check if the connector exists and user has permissions to read it + const connector = await ctx.actions.getActionsClient().get({ id: connectorId }); + if (!connector) { + return res.badRequest({ body: `Connector with id ${connectorId} not found` }); } + + const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); if (retry) { const { updated } = await ruleMigrationsClient.task.updateToRetry( migrationId, @@ -81,19 +85,13 @@ export const registerSiemRuleMigrationsStartRoute = ( return res.noContent(); } - siemMigrationAuditLogger?.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_STARTED, - id: migrationId, - }); + await siemMigrationAuditLogger.logStart({ migrationId }); + return res.ok({ body: { started } }); - } catch (err) { - logger.error(err); - siemMigrationAuditLogger?.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_STARTED, - error: err, - id: migrationId, - }); - return res.badRequest({ body: err.message }); + } catch (error) { + logger.error(error); + await siemMigrationAuditLogger.logStart({ migrationId, error }); + return res.badRequest({ body: error.message }); } } ) diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/stats.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/stats.ts index 4657f2516181c..529eecbac38c6 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/stats.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/stats.ts @@ -13,6 +13,7 @@ import { } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import { SIEM_RULE_MIGRATION_STATS_PATH } from '../../../../../common/siem_migrations/constants'; import type { SecuritySolutionPluginRouter } from '../../../../types'; +import { authz } from './util/authz'; import { withLicense } from './util/with_license'; export const registerSiemRuleMigrationsStatsRoute = ( @@ -23,7 +24,7 @@ export const registerSiemRuleMigrationsStatsRoute = ( .get({ path: SIEM_RULE_MIGRATION_STATS_PATH, access: 'internal', - security: { authz: { requiredPrivileges: ['securitySolution'] } }, + security: { authz }, }) .addVersion( { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/stats_all.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/stats_all.ts index 9ef83d7ab70c2..4e31d5df1076e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/stats_all.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/stats_all.ts @@ -9,6 +9,7 @@ import type { IKibanaResponse, Logger } from '@kbn/core/server'; import type { GetAllStatsRuleMigrationResponse } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import { SIEM_RULE_MIGRATIONS_ALL_STATS_PATH } from '../../../../../common/siem_migrations/constants'; import type { SecuritySolutionPluginRouter } from '../../../../types'; +import { authz } from './util/authz'; import { withLicense } from './util/with_license'; export const registerSiemRuleMigrationsStatsAllRoute = ( @@ -19,7 +20,7 @@ export const registerSiemRuleMigrationsStatsAllRoute = ( .get({ path: SIEM_RULE_MIGRATIONS_ALL_STATS_PATH, access: 'internal', - security: { authz: { requiredPrivileges: ['securitySolution'] } }, + security: { authz }, }) .addVersion( { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/stop.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/stop.ts index c1448d5864952..89e6cc7e58838 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/stop.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/stop.ts @@ -13,7 +13,8 @@ import { type StopRuleMigrationResponse, } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import type { SecuritySolutionPluginRouter } from '../../../../types'; -import { SiemMigrationAuditLogger, SiemMigrationsAuditActions } from './util/audit'; +import { SiemMigrationAuditLogger } from './util/audit'; +import { authz } from './util/authz'; import { withLicense } from './util/with_license'; export const registerSiemRuleMigrationsStopRoute = ( @@ -24,7 +25,7 @@ export const registerSiemRuleMigrationsStopRoute = ( .put({ path: SIEM_RULE_MIGRATION_STOP_PATH, access: 'internal', - security: { authz: { requiredPrivileges: ['securitySolution'] } }, + security: { authz }, }) .addVersion( { @@ -36,13 +37,9 @@ export const registerSiemRuleMigrationsStopRoute = ( withLicense( async (context, req, res): Promise> => { const migrationId = req.params.migration_id; - let siemMigrationAuditLogger: SiemMigrationAuditLogger | undefined; + const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution); try { const ctx = await context.resolve(['securitySolution']); - const auditLogger = ctx.securitySolution.getAuditLogger(); - if (auditLogger) { - siemMigrationAuditLogger = new SiemMigrationAuditLogger(auditLogger); - } const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); const { exists, stopped } = await ruleMigrationsClient.task.stop(migrationId); @@ -50,20 +47,13 @@ export const registerSiemRuleMigrationsStopRoute = ( if (!exists) { return res.noContent(); } - siemMigrationAuditLogger?.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_STOPPED, - id: migrationId, - }); + await siemMigrationAuditLogger.logStop({ migrationId }); return res.ok({ body: { stopped } }); - } catch (err) { - logger.error(err); - siemMigrationAuditLogger?.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_STOPPED, - error: err, - id: migrationId, - }); - return res.badRequest({ body: err.message }); + } catch (error) { + logger.error(error); + await siemMigrationAuditLogger.logStop({ migrationId, error }); + return res.badRequest({ body: error.message }); } } ) diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/translation_stats.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/translation_stats.ts index ede4ccbeaa6d7..aa95d8e898147 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/translation_stats.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/translation_stats.ts @@ -11,6 +11,7 @@ import type { GetRuleMigrationTranslationStatsResponse } from '../../../../../co import { GetRuleMigrationTranslationStatsRequestParams } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import { SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH } from '../../../../../common/siem_migrations/constants'; import type { SecuritySolutionPluginRouter } from '../../../../types'; +import { authz } from './util/authz'; import { withLicense } from './util/with_license'; export const registerSiemRuleMigrationsTranslationStatsRoute = ( @@ -21,7 +22,7 @@ export const registerSiemRuleMigrationsTranslationStatsRoute = ( .get({ path: SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH, access: 'internal', - security: { authz: { requiredPrivileges: ['securitySolution'] } }, + security: { authz }, }) .addVersion( { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/update.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/update.ts index 5d3a2ee088383..1c288578748a6 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/update.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/update.ts @@ -7,13 +7,15 @@ import type { IKibanaResponse, Logger } from '@kbn/core/server'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; -import { SIEM_RULE_MIGRATIONS_PATH } from '../../../../../common/siem_migrations/constants'; +import { SIEM_RULE_MIGRATION_PATH } from '../../../../../common/siem_migrations/constants'; import { UpdateRuleMigrationRequestBody, + UpdateRuleMigrationRequestParams, type UpdateRuleMigrationResponse, } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import type { SecuritySolutionPluginRouter } from '../../../../types'; -import { SiemMigrationAuditLogger, SiemMigrationsAuditActions } from './util/audit'; +import { authz } from './util/authz'; +import { SiemMigrationAuditLogger } from './util/audit'; import { transformToInternalUpdateRuleMigrationData } from './util/update_rules'; import { withLicense } from './util/with_license'; @@ -23,50 +25,44 @@ export const registerSiemRuleMigrationsUpdateRoute = ( ) => { router.versioned .put({ - path: SIEM_RULE_MIGRATIONS_PATH, + path: SIEM_RULE_MIGRATION_PATH, access: 'internal', - security: { authz: { requiredPrivileges: ['securitySolution'] } }, + security: { authz }, }) .addVersion( { version: '1', validate: { - request: { body: buildRouteValidationWithZod(UpdateRuleMigrationRequestBody) }, + request: { + params: buildRouteValidationWithZod(UpdateRuleMigrationRequestParams), + body: buildRouteValidationWithZod(UpdateRuleMigrationRequestBody), + }, }, }, withLicense( async (context, req, res): Promise> => { + const { migration_id: migrationId } = req.params; const rulesToUpdate = req.body; - let siemMigrationAuditLogger: SiemMigrationAuditLogger | undefined; + + const ids = rulesToUpdate.map((rule) => rule.id); + + const siemMigrationAuditLogger = new SiemMigrationAuditLogger(context.securitySolution); try { const ctx = await context.resolve(['securitySolution']); const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); - const auditLogger = ctx.securitySolution.getAuditLogger(); - if (auditLogger) { - siemMigrationAuditLogger = new SiemMigrationAuditLogger(auditLogger); - } - for (const rule of rulesToUpdate) { - siemMigrationAuditLogger?.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_UPDATED_RULE, - id: rule.id, - }); - } + + await siemMigrationAuditLogger.logUpdateRules({ migrationId, ids }); + const transformedRuleToUpdate = rulesToUpdate.map( transformToInternalUpdateRuleMigrationData ); await ruleMigrationsClient.data.rules.update(transformedRuleToUpdate); return res.ok({ body: { updated: true } }); - } catch (err) { - logger.error(err); - for (const rule of rulesToUpdate) { - siemMigrationAuditLogger?.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_UPDATED_RULE, - error: err, - id: rule.id, - }); - } - return res.badRequest({ body: err.message }); + } catch (error) { + logger.error(error); + await siemMigrationAuditLogger.logUpdateRules({ migrationId, ids, error }); + return res.badRequest({ body: error.message }); } } ) diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/audit.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/audit.ts index fa2da6a51cfd8..90a9a330d93e9 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/audit.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/audit.ts @@ -7,19 +7,17 @@ import type { AuditLogger, EcsEvent } from '@kbn/core/server'; import type { ArrayElement } from '@kbn/utility-types'; +import type { SecuritySolutionApiRequestHandlerContext } from '../../../../..'; export enum SiemMigrationsAuditActions { - SIEM_MIGRATION_STARTED = 'siem_migration_started', - SIEM_MIGRATION_STOPPED = 'siem_migration_stopped', SIEM_MIGRATION_CREATED = 'siem_migration_created', - SIEM_MIGRATION_UPDATED = 'siem_migration_updated', SIEM_MIGRATION_RETRIEVED = 'siem_migration_retrieved', - SIEM_MIGRATION_INSTALLED_RULE = 'siem_migration_installed_rule', + SIEM_MIGRATION_UPLOADED_RESOURCES = 'siem_migration_uploaded_resources', + SIEM_MIGRATION_RETRIEVED_RESOURCES = 'siem_migration_retrieved_resources', + SIEM_MIGRATION_STARTED = 'siem_migration_started', + SIEM_MIGRATION_STOPPED = 'siem_migration_stopped', SIEM_MIGRATION_UPDATED_RULE = 'siem_migration_updated_rule', - SIEM_MIGRATION_UPLOADED_MACRO = 'siem_migration_uploaded_macro', - SIEM_MIGRATION_RETRIEVED_MACRO = 'siem_migration_retrieved_macro', - SIEM_MIGRATION_UPLOADED_LOOKUP = 'siem_migration_uploaded_lookup', - SIEM_MIGRATION_RETRIEVED_LOOKUP = 'siem_migration_retrieved_lookup', + SIEM_MIGRATION_INSTALLED_RULES = 'siem_migration_installed_rules', } export enum AUDIT_TYPE { @@ -47,51 +45,39 @@ export const siemMigrationAuditEventType: Record< SiemMigrationsAuditActions, ArrayElement > = { - siem_migration_started: AUDIT_TYPE.START, - siem_migration_stopped: AUDIT_TYPE.END, - siem_migration_created: AUDIT_TYPE.CREATION, - siem_migration_updated: AUDIT_TYPE.CHANGE, - siem_migration_retrieved: AUDIT_TYPE.ACCESS, - siem_migration_installed_rule: AUDIT_TYPE.CREATION, - siem_migration_updated_rule: AUDIT_TYPE.CHANGE, - siem_migration_uploaded_macro: AUDIT_TYPE.CREATION, - siem_migration_retrieved_macro: AUDIT_TYPE.ACCESS, - siem_migration_uploaded_lookup: AUDIT_TYPE.CREATION, - siem_migration_retrieved_lookup: AUDIT_TYPE.ACCESS, -}; - -export const siemMigrationAuditEventMessage: Record = { - siem_migration_started: 'User started an existing SIEM migration', - siem_migration_stopped: 'User stopped an existing SIEM migration', - siem_migration_created: 'User created a new SIEM migration', - siem_migration_updated: 'User updated an existing SIEM migration', - siem_migration_retrieved: 'User retrieved rules from an existing SIEM migration', - siem_migration_installed_rule: 'User installed a new detection rule through SIEM migration', - siem_migration_updated_rule: 'User updated a translated detection rule', - siem_migration_uploaded_macro: 'User uploaded a new macro through SIEM migration', - siem_migration_retrieved_macro: 'User retrieved a SIEM migration macro', - siem_migration_uploaded_lookup: 'User uploaded a new lookup list through SIEM migration', - siem_migration_retrieved_lookup: 'User retrieved a SIEM migration lookup list', + [SiemMigrationsAuditActions.SIEM_MIGRATION_CREATED]: AUDIT_TYPE.CREATION, + [SiemMigrationsAuditActions.SIEM_MIGRATION_RETRIEVED]: AUDIT_TYPE.ACCESS, + [SiemMigrationsAuditActions.SIEM_MIGRATION_UPLOADED_RESOURCES]: AUDIT_TYPE.CREATION, + [SiemMigrationsAuditActions.SIEM_MIGRATION_RETRIEVED_RESOURCES]: AUDIT_TYPE.ACCESS, + [SiemMigrationsAuditActions.SIEM_MIGRATION_STARTED]: AUDIT_TYPE.START, + [SiemMigrationsAuditActions.SIEM_MIGRATION_STOPPED]: AUDIT_TYPE.END, + [SiemMigrationsAuditActions.SIEM_MIGRATION_UPDATED_RULE]: AUDIT_TYPE.CHANGE, + [SiemMigrationsAuditActions.SIEM_MIGRATION_INSTALLED_RULES]: AUDIT_TYPE.CREATION, }; -export interface SiemMigrationAuditEvent { +interface SiemMigrationAuditEvent { action: SiemMigrationsAuditActions; + message: string; error?: Error; - id?: string; } export class SiemMigrationAuditLogger { - constructor(private readonly auditLogger: AuditLogger) {} - - public log({ action, error, id }: SiemMigrationAuditEvent): void { - const type = siemMigrationAuditEventType[action]; - let message = siemMigrationAuditEventMessage[action]; + private auditLogger?: AuditLogger | null = null; + constructor( + private readonly securitySolutionContextPromise: Promise + ) {} - if (id) { - message += ` with [id=${id}]`; + private setAuditLogger = async (): Promise => { + if (this.auditLogger === null) { + const securitySolutionContext = await this.securitySolutionContextPromise; + this.auditLogger = securitySolutionContext.getAuditLogger(); } + return !!this.auditLogger; + }; - this.auditLogger.log({ + private logEvent = ({ action, message, error }: SiemMigrationAuditEvent): void => { + const type = siemMigrationAuditEventType[action]; + this.auditLogger?.log({ message, event: { action, @@ -104,5 +90,104 @@ export class SiemMigrationAuditLogger { message: error.message, }, }); + }; + + private async log(event: SiemMigrationAuditEvent | SiemMigrationAuditEvent[]): Promise { + const auditLoggerSet = await this.setAuditLogger(); + if (!auditLoggerSet) { + // Audit logger is not available + return; + } + + if (Array.isArray(event)) { + event.forEach((e) => this.logEvent(e)); + } else { + this.logEvent(event); + } + } + + public async logCreateMigration(params: { migrationId: string; error?: Error }): Promise { + const { migrationId, error } = params; + const message = `User created a new SIEM migration with [id=${migrationId}]`; + return this.log({ + action: SiemMigrationsAuditActions.SIEM_MIGRATION_CREATED, + message, + error, + }); + } + + public async logGetMigration(params: { migrationId: string; error?: Error }): Promise { + const { migrationId, error } = params; + const message = `User retrieved the SIEM migration with [id=${migrationId}]`; + return this.log({ + action: SiemMigrationsAuditActions.SIEM_MIGRATION_RETRIEVED, + message, + error, + }); + } + + public async logUploadResources(params: { migrationId: string; error?: Error }): Promise { + const { migrationId, error } = params; + const message = `User uploaded resources to the SIEM migration with [id=${migrationId}]`; + return this.log({ + action: SiemMigrationsAuditActions.SIEM_MIGRATION_UPLOADED_RESOURCES, + message, + error, + }); + } + + public async logGetResources(params: { migrationId: string; error?: Error }): Promise { + const { migrationId, error } = params; + const message = `User retrieved resources from the SIEM migration with [id=${migrationId}]`; + return this.log({ + action: SiemMigrationsAuditActions.SIEM_MIGRATION_RETRIEVED_RESOURCES, + message, + error, + }); + } + + public async logStart(params: { migrationId: string; error?: Error }): Promise { + const { migrationId, error } = params; + const message = `User stopped the SIEM rules migration with [id=${migrationId}]`; + return this.log({ action: SiemMigrationsAuditActions.SIEM_MIGRATION_STARTED, message, error }); + } + + public async logStop(params: { migrationId: string; error?: Error }): Promise { + const { migrationId, error } = params; + const message = `User stopped the SIEM rules migration with [id=${migrationId}]`; + return this.log({ action: SiemMigrationsAuditActions.SIEM_MIGRATION_STOPPED, message, error }); + } + + public async logUpdateRules(params: { + migrationId: string; + ids: string[]; + error?: Error; + }): Promise { + const { ids, migrationId, error } = params; + const events = ids.map((id) => { + const message = `User updated a translated rule through SIEM migration with [id=${id}, migration_id=${migrationId}]`; + return { action: SiemMigrationsAuditActions.SIEM_MIGRATION_UPDATED_RULE, message, error }; + }); + return this.log(events); + } + + public async logInstallRules(params: { + migrationId: string; + ids?: string[]; + error?: Error; + }): Promise { + const { ids, migrationId, error } = params; + const action = SiemMigrationsAuditActions.SIEM_MIGRATION_INSTALLED_RULES; + const events: SiemMigrationAuditEvent[] = []; + if (ids) { + ids.forEach((id) => { + const message = `User installed a translated rule through SIEM migration with [id=${id}, migration_id=${migrationId}]`; + events.push({ action, message, error }); + }); + } else { + const message = `User installed all installable translated rules through SIEM migration with [migration_id=${migrationId}]`; + events.push({ action, message, error }); + } + return this.log(events); } } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/authz.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/authz.ts new file mode 100644 index 0000000000000..7131dc57f911a --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/authz.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 { SIEM_MIGRATIONS_API_ACTION_ALL } from '@kbn/security-solution-features/actions'; + +export const authz = { + requiredPrivileges: ['securitySolution', SIEM_MIGRATIONS_API_ACTION_ALL], +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_lookups_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_lookups_client.ts index 24efc3ae7eb87..0300b6f8b720e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_lookups_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_lookups_client.ts @@ -8,6 +8,7 @@ import { sha256 } from 'js-sha256'; import type { AuthenticatedUser, IScopedClusterClient, Logger } from '@kbn/core/server'; import { retryTransientEsErrors } from '@kbn/index-adapter'; +import { LOOKUPS_INDEX_PREFIX } from '../../../../../common/siem_migrations/constants'; export type LookupData = object[]; @@ -19,7 +20,7 @@ export class RuleMigrationsDataLookupsClient { ) {} async create(lookupName: string, data: LookupData): Promise { - const indexName = `lookup_${lookupName}`; + const indexName = `${LOOKUPS_INDEX_PREFIX}${lookupName}`; try { await this.executeEs(() => this.esScopedClient.asCurrentUser.indices.create({ diff --git a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/index.ts b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/index.ts index a29ea4b9e2833..8370863321226 100644 --- a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/index.ts +++ b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/index.ts @@ -5,24 +5,28 @@ * 2.0. */ -import type { ProductFeatureKeys } from '@kbn/security-solution-features'; -import type { ProductFeaturesConfigurator } from '@kbn/security-solution-plugin/server/lib/product_features_service/types'; +import type { + ProductFeatureKeys, + ProductFeaturesConfigurator, +} from '@kbn/security-solution-features'; import { getCasesProductFeaturesConfigurator } from './cases_product_features_config'; import { getSecurityProductFeaturesConfigurator } from './security_product_features_config'; import { getSecurityAssistantProductFeaturesConfigurator } from './assistant_product_features_config'; import { getAttackDiscoveryProductFeaturesConfigurator } from './attack_discovery_product_features_config'; import { getTimelineProductFeaturesConfigurator } from './timeline_product_features_config'; import { getNotesProductFeaturesConfigurator } from './notes_product_features_config'; +import { getSiemMigrationsProductFeaturesConfigurator } from './siem_migrations_product_features_config'; export const getProductProductFeaturesConfigurator = ( enabledProductFeatureKeys: ProductFeatureKeys ): ProductFeaturesConfigurator => { return { - attackDiscovery: getAttackDiscoveryProductFeaturesConfigurator(enabledProductFeatureKeys), security: getSecurityProductFeaturesConfigurator(enabledProductFeatureKeys), cases: getCasesProductFeaturesConfigurator(enabledProductFeatureKeys), securityAssistant: getSecurityAssistantProductFeaturesConfigurator(enabledProductFeatureKeys), + attackDiscovery: getAttackDiscoveryProductFeaturesConfigurator(enabledProductFeatureKeys), timeline: getTimelineProductFeaturesConfigurator(enabledProductFeatureKeys), notes: getNotesProductFeaturesConfigurator(enabledProductFeatureKeys), + siemMigrations: getSiemMigrationsProductFeaturesConfigurator(enabledProductFeatureKeys), }; }; diff --git a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/siem_migrations_product_features_config.ts b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/siem_migrations_product_features_config.ts new file mode 100644 index 0000000000000..6e42f7009cdbf --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/siem_migrations_product_features_config.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + ProductFeatureKeys, + ProductFeatureKibanaConfig, + ProductFeaturesSiemMigrationsConfig, +} from '@kbn/security-solution-features'; +import { + siemMigrationsDefaultProductFeaturesConfig, + createEnabledProductFeaturesConfigMap, +} from '@kbn/security-solution-features/config'; +import type { ProductFeatureSiemMigrationsKey } from '@kbn/security-solution-features/keys'; + +/** + * Maps the ProductFeatures keys to Kibana privileges that will be merged + * into the base privileges config for the Security app. + * + * Privileges can be added in different ways: + * - `privileges`: the privileges that will be added directly into the main Attack discovery feature. + * - `subFeatureIds`: the ids of the sub-features that will be added into the Attack discovery subFeatures entry. + * - `subFeaturesPrivileges`: the privileges that will be added into the existing Attack discovery subFeature with the privilege `id` specified. + */ +const siemMigrationsProductFeaturesConfig: Record< + ProductFeatureSiemMigrationsKey, + ProductFeatureKibanaConfig +> = { + ...siemMigrationsDefaultProductFeaturesConfig, + // ess-specific app features configs here +}; + +export const getSiemMigrationsProductFeaturesConfigurator = + (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesSiemMigrationsConfig => + createEnabledProductFeaturesConfigMap( + siemMigrationsProductFeaturesConfig, + enabledProductFeatureKeys + ); diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/common/pli/pli_config.ts b/x-pack/solutions/security/plugins/security_solution_serverless/common/pli/pli_config.ts index 342125d661f40..77899352ab104 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/common/pli/pli_config.ts +++ b/x-pack/solutions/security/plugins/security_solution_serverless/common/pli/pli_config.ts @@ -30,6 +30,7 @@ export const PLI_PRODUCT_FEATURES: PliProductFeatures = { ProductFeatureKey.externalRuleActions, ProductFeatureKey.automaticImport, ProductFeatureKey.prebuiltRuleCustomization, + ProductFeatureKey.siemMigrations, ], }, endpoint: { diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/index.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/index.ts index 87320cca5ae9a..f6eec1a11e0d3 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/index.ts +++ b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/index.ts @@ -9,12 +9,13 @@ import type { Logger } from '@kbn/logging'; import { ProductFeatureKey } from '@kbn/security-solution-features/keys'; import type { ProductFeatureKeys } from '@kbn/security-solution-features'; -import { getAttackDiscoveryProductFeaturesConfigurator } from './attack_discovery_product_features_config'; import { getCasesProductFeaturesConfigurator } from './cases_product_features_config'; import { getSecurityProductFeaturesConfigurator } from './security_product_features_config'; import { getSecurityAssistantProductFeaturesConfigurator } from './assistant_product_features_config'; +import { getAttackDiscoveryProductFeaturesConfigurator } from './attack_discovery_product_features_config'; import { getTimelineProductFeaturesConfigurator } from './timeline_product_features_config'; import { getNotesProductFeaturesConfigurator } from './notes_product_features_config'; +import { getSiemMigrationsProductFeaturesConfigurator } from './siem_migrations_product_features_config'; import { enableRuleActions } from '../rules/enable_rule_actions'; import type { ServerlessSecurityConfig } from '../config'; import type { Tier, SecuritySolutionServerlessPluginSetupDeps } from '../types'; @@ -35,15 +36,16 @@ export const registerProductFeatures = ( // register product features for the main security solution product features service pluginsSetup.securitySolution.setProductFeaturesConfigurator({ - attackDiscovery: getAttackDiscoveryProductFeaturesConfigurator(enabledProductFeatureKeys), security: getSecurityProductFeaturesConfigurator( enabledProductFeatureKeys, config.experimentalFeatures ), cases: getCasesProductFeaturesConfigurator(enabledProductFeatureKeys), securityAssistant: getSecurityAssistantProductFeaturesConfigurator(enabledProductFeatureKeys), + attackDiscovery: getAttackDiscoveryProductFeaturesConfigurator(enabledProductFeatureKeys), timeline: getTimelineProductFeaturesConfigurator(enabledProductFeatureKeys), notes: getNotesProductFeaturesConfigurator(enabledProductFeatureKeys), + siemMigrations: getSiemMigrationsProductFeaturesConfigurator(enabledProductFeatureKeys), }); // enable rule actions based on the enabled product features diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/siem_migrations_product_features_config.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/siem_migrations_product_features_config.ts new file mode 100644 index 0000000000000..b6bcb93c8f8ad --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/siem_migrations_product_features_config.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { + ProductFeatureKeys, + ProductFeatureKibanaConfig, + ProductFeaturesSiemMigrationsConfig, +} from '@kbn/security-solution-features'; +import { + siemMigrationsDefaultProductFeaturesConfig, + createEnabledProductFeaturesConfigMap, +} from '@kbn/security-solution-features/config'; +import type { ProductFeatureSiemMigrationsKey } from '@kbn/security-solution-features/keys'; + +/** + * Maps the ProductFeatures keys to Kibana privileges that will be merged + * into the base privileges config for the app. + * + * Privileges can be added in different ways: + * - `privileges`: the privileges that will be added directly into the main Attack discovery feature. + * - `subFeatureIds`: the ids of the sub-features that will be added into the Attack discovery subFeatures entry. + * - `subFeaturesPrivileges`: the privileges that will be added into the existing Attack discovery subFeature with the privilege `id` specified. + */ +const siemMigrationsProductFeaturesConfig: Record< + ProductFeatureSiemMigrationsKey, + ProductFeatureKibanaConfig +> = { + ...siemMigrationsDefaultProductFeaturesConfig, + // serverless-specific app features configs here +}; + +export const getSiemMigrationsProductFeaturesConfigurator = + (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesSiemMigrationsConfig => + createEnabledProductFeaturesConfigMap( + siemMigrationsProductFeaturesConfig, + enabledProductFeatureKeys + ); diff --git a/x-pack/test/api_integration/apis/features/features/features.ts b/x-pack/test/api_integration/apis/features/features/features.ts index d451bec90aaa3..6410bfe5bbb1e 100644 --- a/x-pack/test/api_integration/apis/features/features/features.ts +++ b/x-pack/test/api_integration/apis/features/features/features.ts @@ -140,6 +140,7 @@ export default function ({ getService }: FtrProviderContext) { 'securitySolutionCasesV3', 'securitySolutionTimeline', 'securitySolutionNotes', + 'securitySolutionSiemMigrations', 'fleet', 'fleetv2', 'entityManager', @@ -198,6 +199,7 @@ export default function ({ getService }: FtrProviderContext) { 'securitySolutionCasesV3', 'securitySolutionTimeline', 'securitySolutionNotes', + 'securitySolutionSiemMigrations', 'fleet', 'fleetv2', 'entityManager', diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index bf356cfa5037e..dae2b83f79442 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -202,6 +202,7 @@ export default function ({ getService }: FtrProviderContext) { ], securitySolutionTimeline: ['all', 'read', 'minimal_all', 'minimal_read'], securitySolutionNotes: ['all', 'read', 'minimal_all', 'minimal_read'], + securitySolutionSiemMigrations: ['all', 'read', 'minimal_all', 'minimal_read'], infrastructure: ['all', 'read', 'minimal_all', 'minimal_read'], logs: ['all', 'read', 'minimal_all', 'minimal_read'], dataQuality: ['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 70ad6564cd565..7625174bda827 100644 --- a/x-pack/test/api_integration/apis/security/privileges_basic.ts +++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts @@ -62,6 +62,7 @@ export default function ({ getService }: FtrProviderContext) { securitySolutionCasesV3: ['all', 'read', 'minimal_all', 'minimal_read'], securitySolutionNotes: ['all', 'read', 'minimal_all', 'minimal_read'], securitySolutionTimeline: ['all', 'read', 'minimal_all', 'minimal_read'], + securitySolutionSiemMigrations: ['all', 'read', 'minimal_all', 'minimal_read'], searchPlayground: ['all', 'read', 'minimal_all', 'minimal_read'], searchSynonyms: ['all', 'read', 'minimal_all', 'minimal_read'], searchInferenceEndpoints: ['all', 'read', 'minimal_all', 'minimal_read'], @@ -301,6 +302,7 @@ export default function ({ getService }: FtrProviderContext) { 'cases_assign', ], securitySolutionTimeline: ['all', 'read', 'minimal_all', 'minimal_read'], + securitySolutionSiemMigrations: ['all', 'read', 'minimal_all', 'minimal_read'], securitySolutionNotes: ['all', 'read', 'minimal_all', 'minimal_read'], infrastructure: ['all', 'read', 'minimal_all', 'minimal_read'], logs: ['all', 'read', 'minimal_all', 'minimal_read'], diff --git a/x-pack/test/api_integration/services/security_solution_api.gen.ts b/x-pack/test/api_integration/services/security_solution_api.gen.ts index b3b0eb4571674..6cdf64afab48f 100644 --- a/x-pack/test/api_integration/services/security_solution_api.gen.ts +++ b/x-pack/test/api_integration/services/security_solution_api.gen.ts @@ -156,7 +156,10 @@ import { StopRuleMigrationRequestParamsInput } from '@kbn/security-solution-plug import { SuggestUserProfilesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/users/suggest_user_profiles_route.gen'; import { TriggerRiskScoreCalculationRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/risk_engine/entity_calculation_route.gen'; import { UpdateRuleRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/update_rule/update_rule_route.gen'; -import { UpdateRuleMigrationRequestBodyInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; +import { + UpdateRuleMigrationRequestParamsInput, + UpdateRuleMigrationRequestBodyInput, +} from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; import { UpdateWorkflowInsightRequestParamsInput, UpdateWorkflowInsightRequestBodyInput, @@ -1005,6 +1008,16 @@ finalize it. .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); }, + /** + * Identifies the privileges required for a SIEM rules migration and returns the missing privileges + */ + getRuleMigrationPrivileges(kibanaSpace: string = 'default') { + return supertest + .get(routeWithNamespace('/internal/siem_migrations/rules/missing_privileges', kibanaSpace)) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, /** * Retrieves resources for an existing SIEM rules migration */ @@ -1577,7 +1590,12 @@ detection engine rules. */ updateRuleMigration(props: UpdateRuleMigrationProps, kibanaSpace: string = 'default') { return supertest - .put(routeWithNamespace('/internal/siem_migrations/rules', kibanaSpace)) + .put( + routeWithNamespace( + replaceParams('/internal/siem_migrations/rules/{migration_id}', props.params), + kibanaSpace + ) + ) .set('kbn-xsrf', 'true') .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') @@ -1914,6 +1932,7 @@ export interface UpdateRuleProps { body: UpdateRuleRequestBodyInput; } export interface UpdateRuleMigrationProps { + params: UpdateRuleMigrationRequestParamsInput; body: UpdateRuleMigrationRequestBodyInput; } export interface UpdateWorkflowInsightProps { diff --git a/x-pack/test/spaces_api_integration/common/suites/create.agnostic.ts b/x-pack/test/spaces_api_integration/common/suites/create.agnostic.ts index 58d0b5c5d5380..84d66568c5f36 100644 --- a/x-pack/test/spaces_api_integration/common/suites/create.agnostic.ts +++ b/x-pack/test/spaces_api_integration/common/suites/create.agnostic.ts @@ -97,6 +97,7 @@ export function createTestSuiteFactory({ getService }: DeploymentAgnosticFtrProv 'securitySolutionCasesV2', 'securitySolutionCasesV3', 'securitySolutionNotes', + 'securitySolutionSiemMigrations', 'securitySolutionTimeline', 'siem', 'siemV2', diff --git a/x-pack/test/spaces_api_integration/common/suites/get.agnostic.ts b/x-pack/test/spaces_api_integration/common/suites/get.agnostic.ts index a77486d4b27cf..b30234c8f0e4d 100644 --- a/x-pack/test/spaces_api_integration/common/suites/get.agnostic.ts +++ b/x-pack/test/spaces_api_integration/common/suites/get.agnostic.ts @@ -101,6 +101,7 @@ export function getTestSuiteFactory(context: DeploymentAgnosticFtrProviderContex 'securitySolutionCasesV2', 'securitySolutionCasesV3', 'securitySolutionNotes', + 'securitySolutionSiemMigrations', 'securitySolutionTimeline', 'siem', 'siemV2', diff --git a/x-pack/test/spaces_api_integration/common/suites/get_all.agnostic.ts b/x-pack/test/spaces_api_integration/common/suites/get_all.agnostic.ts index bf315f3bdb112..283455c529373 100644 --- a/x-pack/test/spaces_api_integration/common/suites/get_all.agnostic.ts +++ b/x-pack/test/spaces_api_integration/common/suites/get_all.agnostic.ts @@ -89,6 +89,7 @@ const ALL_SPACE_RESULTS: Space[] = [ 'securitySolutionCasesV2', 'securitySolutionCasesV3', 'securitySolutionNotes', + 'securitySolutionSiemMigrations', 'securitySolutionTimeline', 'siem', 'siemV2', diff --git a/x-pack/test/spaces_api_integration/spaces_only/telemetry/telemetry.ts b/x-pack/test/spaces_api_integration/spaces_only/telemetry/telemetry.ts index 52b4440eeee56..141422ddb6d44 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/telemetry/telemetry.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/telemetry/telemetry.ts @@ -100,6 +100,7 @@ export default function ({ getService }: FtrProviderContext) { securitySolutionAttackDiscovery: 0, securitySolutionTimeline: 0, securitySolutionNotes: 0, + securitySolutionSiemMigrations: 0, discover: 0, discover_v2: 0, visualize: 0, diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts index 75408726ab8c9..d96c5f3039a1a 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts @@ -66,6 +66,7 @@ export default function navLinksTests({ getService }: FtrProviderContext) { 'guidedOnboardingFeature', 'securitySolutionAssistant', 'securitySolutionAttackDiscovery', + 'securitySolutionSiemMigrations', 'dataQuality' ) );