From b07a8c0a9dd79f4172983d7af7bbb99ec7a88f3a Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Fri, 17 Jan 2025 15:37:37 +0100 Subject: [PATCH 01/25] create siem migrations feature --- .../security/packages/features/config.ts | 1 + .../packages/features/product_features.ts | 1 + .../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 | 11 +++- .../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 | 14 ++++ .../onboarding/components/onboarding.tsx | 20 +----- .../components/onboarding_route.tsx | 38 ----------- .../components/onboarding_router.tsx | 66 +++++++++++++++++++ .../product_features_service.test.ts | 24 +++++-- .../product_features_service.ts | 22 ++++++- .../lib/product_features_service/types.ts | 20 ------ .../server/product_features/index.ts | 10 ++- ...siem_migrations_product_features_config.ts | 41 ++++++++++++ .../server/product_features/index.ts | 6 +- ...siem_migrations_product_features_config.ts | 40 +++++++++++ 22 files changed, 329 insertions(+), 93 deletions(-) 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 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 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_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/config.ts b/x-pack/solutions/security/packages/features/config.ts index 9fa14ab0ba7c2..d004dac146dcf 100644 --- a/x-pack/solutions/security/packages/features/config.ts +++ b/x-pack/solutions/security/packages/features/config.ts @@ -9,5 +9,6 @@ export { securityDefaultProductFeaturesConfig } from './src/security/product_fea export { getCasesDefaultProductFeaturesConfig } from './src/cases/product_feature_config'; export { assistantDefaultProductFeaturesConfig } from './src/assistant/product_feature_config'; export { attackDiscoveryDefaultProductFeaturesConfig } from './src/attack_discovery/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/product_features.ts b/x-pack/solutions/security/packages/features/product_features.ts index 67d61f21fae5e..2bc7e69e8138e 100644 --- a/x-pack/solutions/security/packages/features/product_features.ts +++ b/x-pack/solutions/security/packages/features/product_features.ts @@ -9,3 +9,4 @@ export { getSecurityFeature } from './src/security'; export { getCasesFeature, getCasesV2Feature } from './src/cases'; export { getAssistantFeature } from './src/assistant'; export { getAttackDiscoveryFeature } from './src/attack_discovery'; +export { getSiemMigrationsFeature } from './src/siem_migrations'; 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 db442d894363a..dc69c4a8d640e 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 c0c025335d054..a75ab99328b09 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 @@ -26,7 +26,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 c6acab28c4860..027c7497deaa9 100644 --- a/x-pack/solutions/security/packages/features/src/constants.ts +++ b/x-pack/solutions/security/packages/features/src/constants.ts @@ -21,6 +21,7 @@ export const SECURITY_SOLUTION_CASES_APP_ID = 'securitySolutionCases' as const; export const ASSISTANT_FEATURE_ID = 'securitySolutionAssistant' as const; export const ATTACK_DISCOVERY_FEATURE_ID = 'securitySolutionAttackDiscovery' 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 bbc92ae0d978e..2f30a94a4cd36 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 @@ -114,19 +114,28 @@ export enum ProductFeatureAttackDiscoveryKey { attackDiscovery = 'attack_discovery', } +export enum ProductFeatureSiemMigrationsKey { + /** + * Enables the SIEM Migrations main feature + */ + siemMigrations = 'siem_migrations', +} + // Merges the two enums. export const ProductFeatureKey = { ...ProductFeatureSecurityKey, ...ProductFeatureCasesKey, ...ProductFeatureAssistantKey, ...ProductFeatureAttackDiscoveryKey, + ...ProductFeatureSiemMigrationsKey, }; // We need to merge the value and the type and export both to replicate how enum works. export type ProductFeatureKeyType = | ProductFeatureSecurityKey | ProductFeatureCasesKey | ProductFeatureAssistantKey - | ProductFeatureAttackDiscoveryKey; + | ProductFeatureAttackDiscoveryKey + | ProductFeatureSiemMigrationsKey; export const ALL_PRODUCT_FEATURE_KEYS = Object.freeze(Object.values(ProductFeatureKey)); 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..8e7ce9812fab4 --- /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_FEATURE_ID } from '../constants'; +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_FEATURE_ID], + ui: ['siem-migrations'], + }, + }, + }, +}; diff --git a/x-pack/solutions/security/packages/features/src/types.ts b/x-pack/solutions/security/packages/features/src/types.ts index 17e4869fa66f1..85c0da66ca6bb 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, } from './product_features_keys'; export type { ProductFeatureKeyType }; @@ -57,6 +58,11 @@ export type ProductFeaturesAttackDiscoveryConfig = Map< ProductFeatureKibanaConfig >; +export type ProductFeaturesSiemMigrationsConfig = Map< + ProductFeatureSiemMigrationsKey, + ProductFeatureKibanaConfig +>; + export type AppSubFeaturesMap = Map; export interface ProductFeatureParams { @@ -64,3 +70,11 @@ export interface ProductFeatureParams { baseKibanaSubFeatureIds: T[]; subFeaturesMap: AppSubFeaturesMap; } + +export interface ProductFeaturesConfigurator { + security: () => ProductFeaturesConfig; + cases: () => ProductFeaturesConfig; + securityAssistant: () => ProductFeaturesConfig; + attackDiscovery: () => ProductFeaturesConfig; + siemMigrations: () => ProductFeaturesConfig; +} 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_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/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 768228f319b24..4227b10a70302 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,11 +41,12 @@ const productFeature = { }; const mockGetFeature = jest.fn().mockReturnValue(productFeature); jest.mock('@kbn/security-solution-features/product_features', () => ({ - getAttackDiscoveryFeature: () => mockGetFeature(), - getAssistantFeature: () => mockGetFeature(), + getSecurityFeature: () => mockGetFeature(), getCasesFeature: () => mockGetFeature(), getCasesV2Feature: () => mockGetFeature(), - getSecurityFeature: () => mockGetFeature(), + getAssistantFeature: () => mockGetFeature(), + getAttackDiscoveryFeature: () => mockGetFeature(), + getSiemMigrationsFeature: () => mockGetFeature(), })); describe('ProductFeaturesService', () => { @@ -90,12 +91,14 @@ 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 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), }; productFeaturesService.setProductFeaturesConfigurator(configurator); @@ -103,6 +106,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 @@ -114,6 +118,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', () => { @@ -139,12 +146,16 @@ describe('ProductFeaturesService', () => { const mockAttackDiscoveryConfig = new Map([ [ProductFeatureKey.attackDiscovery, {}], ]) as ProductFeaturesConfig; + const mockSiemMigrationsConfig = new Map([ + [ProductFeatureKey.siemMigrations, {}], + ]) 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), }; productFeaturesService.setProductFeaturesConfigurator(configurator); @@ -153,6 +164,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 2901734527a93..4aad093f62c23 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 @@ -14,19 +14,22 @@ 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, getCasesFeature, getSecurityFeature, getCasesV2Feature, + getSiemMigrationsFeature, } from '@kbn/security-solution-features/product_features'; 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 } from './security_saved_objects'; import { casesApiTags, casesUiCapabilities } from './cases_privileges'; @@ -39,6 +42,8 @@ export class ProductFeaturesService { private casesProductV2Features: ProductFeatures; private securityAssistantProductFeatures: ProductFeatures; private attackDiscoveryProductFeatures: ProductFeatures; + private siemMigrationsProductFeatures: ProductFeatures; + private productFeatures?: Set; constructor( @@ -97,6 +102,14 @@ export class ProductFeaturesService { attackDiscoveryFeature.baseKibanaFeature, attackDiscoveryFeature.baseKibanaSubFeatureIds ); + + const siemMigrationsFeature = getSiemMigrationsFeature(); + this.siemMigrationsProductFeatures = new ProductFeatures( + this.logger, + siemMigrationsFeature.subFeaturesMap, + siemMigrationsFeature.baseKibanaFeature, + siemMigrationsFeature.baseKibanaSubFeatureIds + ); } public init(featuresSetup: FeaturesPluginSetup) { @@ -105,6 +118,7 @@ export class ProductFeaturesService { this.casesProductV2Features.init(featuresSetup); this.securityAssistantProductFeatures.init(featuresSetup); this.attackDiscoveryProductFeatures.init(featuresSetup); + this.siemMigrationsProductFeatures.init(featuresSetup); } public setProductFeaturesConfigurator(configurator: ProductFeaturesConfigurator) { @@ -121,12 +135,16 @@ export class ProductFeaturesService { const attackDiscoveryProductFeaturesConfig = configurator.attackDiscovery(); this.attackDiscoveryProductFeatures.setConfig(attackDiscoveryProductFeaturesConfig); + const siemMigrationsProductFeaturesConfig = configurator.siemMigrations(); + this.siemMigrationsProductFeatures.setConfig(siemMigrationsProductFeaturesConfig); + this.productFeatures = new Set( Object.freeze([ ...securityProductFeaturesConfig.keys(), ...casesProductFeaturesConfig.keys(), ...securityAssistantProductFeaturesConfig.keys(), ...attackDiscoveryProductFeaturesConfig.keys(), + ...siemMigrationsProductFeaturesConfig.keys(), ]) as readonly ProductFeatureKeyType[] ); } 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 9c7b20cfba960..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/types.ts +++ /dev/null @@ -1,20 +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; -} 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 ed85c32f12284..32d8d943b8f07 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,20 +5,24 @@ * 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 { 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), + 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/server/product_features/index.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/index.ts index 310ea860787ba..c9cbaee3448d0 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,10 +9,11 @@ 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 { 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'; @@ -33,13 +34,14 @@ 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), + 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 + ); From ec90eeb4a9692f584cd3556dc6a80054239a3a4f Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Fri, 17 Jan 2025 15:40:54 +0100 Subject: [PATCH 02/25] remove nested field mapping for original and elastic rules --- .../rules/data/rule_migrations_field_maps.ts | 13 ++++-- .../lib/siem_migrations/rules/data/search.ts | 41 ++++--------------- .../lib/siem_migrations/rules/data/sort.ts | 7 +--- 3 files changed, 19 insertions(+), 42 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts index 17370d51dbb71..fd8e27e61db01 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts @@ -12,12 +12,20 @@ import type { } from '../../../../../common/siem_migrations/model/rule_migration.gen'; import type { RuleMigrationIntegration, RuleMigrationPrebuiltRule } from '../types'; -export const ruleMigrationsFieldMap: FieldMap>> = { +/** + * The `id` is not indexed, we assign _id. + * The `original_rule` and `elastic_rule` are regular objects, no need to define them as nested type + **/ +type RuleMigrationKeys = Exclude< + SchemaFieldMapKeys, + 'id' | 'original_rule' | 'elastic_rule' +>; + +export const ruleMigrationsFieldMap: FieldMap = { '@timestamp': { type: 'date', required: false }, migration_id: { type: 'keyword', required: true }, created_by: { type: 'keyword', required: true }, status: { type: 'keyword', required: true }, - original_rule: { type: 'nested', required: true }, 'original_rule.vendor': { type: 'keyword', required: true }, 'original_rule.id': { type: 'keyword', required: true }, 'original_rule.title': { type: 'text', required: true, fields: { keyword: { type: 'keyword' } } }, @@ -26,7 +34,6 @@ export const ruleMigrationsFieldMap: FieldMap Date: Fri, 17 Jan 2025 16:45:19 +0100 Subject: [PATCH 03/25] define fields as object --- .../rules/data/rule_migrations_field_maps.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts index fd8e27e61db01..12a10f38a7e12 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts @@ -12,28 +12,21 @@ import type { } from '../../../../../common/siem_migrations/model/rule_migration.gen'; import type { RuleMigrationIntegration, RuleMigrationPrebuiltRule } from '../types'; -/** - * The `id` is not indexed, we assign _id. - * The `original_rule` and `elastic_rule` are regular objects, no need to define them as nested type - **/ -type RuleMigrationKeys = Exclude< - SchemaFieldMapKeys, - 'id' | 'original_rule' | 'elastic_rule' ->; - -export const ruleMigrationsFieldMap: FieldMap = { +export const ruleMigrationsFieldMap: FieldMap>> = { '@timestamp': { type: 'date', required: false }, migration_id: { type: 'keyword', required: true }, created_by: { type: 'keyword', required: true }, status: { type: 'keyword', required: true }, + original_rule: { type: 'object', required: true }, 'original_rule.vendor': { type: 'keyword', required: true }, 'original_rule.id': { type: 'keyword', required: true }, 'original_rule.title': { type: 'text', required: true, fields: { keyword: { type: 'keyword' } } }, 'original_rule.description': { type: 'text', required: false }, 'original_rule.query': { type: 'text', required: true }, 'original_rule.query_language': { type: 'keyword', required: true }, - 'original_rule.annotations': { type: 'nested', required: false }, + 'original_rule.annotations': { type: 'object', required: false }, 'original_rule.annotations.mitre_attack': { type: 'keyword', array: true, required: false }, + elastic_rule: { type: 'object', required: false }, 'elastic_rule.title': { type: 'text', required: true, fields: { keyword: { type: 'keyword' } } }, 'elastic_rule.integration_ids': { type: 'keyword', required: false, array: true }, 'elastic_rule.query': { type: 'text', required: true }, From 899191054ddba7f7e7bcccf61ff4543180b72325 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Fri, 17 Jan 2025 17:14:36 +0100 Subject: [PATCH 04/25] always use conditions instead of this --- .../lib/siem_migrations/rules/data/search.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/search.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/search.ts index 466f0e3fa8c06..40281c77da412 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/search.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/data/search.ts @@ -16,40 +16,40 @@ export const conditions = { return { term: { translation_result: RuleTranslationResult.FULL } }; }, isNotFullyTranslated(): QueryDslQueryContainer { - return { bool: { must_not: this.isFullyTranslated() } }; + return { bool: { must_not: conditions.isFullyTranslated() } }; }, isPartiallyTranslated(): QueryDslQueryContainer { return { term: { translation_result: RuleTranslationResult.PARTIAL } }; }, isNotPartiallyTranslated(): QueryDslQueryContainer { - return { bool: { must_not: this.isPartiallyTranslated() } }; + return { bool: { must_not: conditions.isPartiallyTranslated() } }; }, isUntranslatable(): QueryDslQueryContainer { return { term: { translation_result: RuleTranslationResult.UNTRANSLATABLE } }; }, isNotUntranslatable(): QueryDslQueryContainer { - return { bool: { must_not: this.isUntranslatable() } }; + return { bool: { must_not: conditions.isUntranslatable() } }; }, isInstalled(): QueryDslQueryContainer { return { exists: { field: 'elastic_rule.id' } }; }, isNotInstalled(): QueryDslQueryContainer { - return { bool: { must_not: this.isInstalled() } }; + return { bool: { must_not: conditions.isInstalled() } }; }, isPrebuilt(): QueryDslQueryContainer { return { exists: { field: 'elastic_rule.prebuilt_rule_id' } }; }, isCustom(): QueryDslQueryContainer { - return { bool: { must_not: this.isPrebuilt() } }; + return { bool: { must_not: conditions.isPrebuilt() } }; }, matchTitle(title: string): QueryDslQueryContainer { return { match: { 'elastic_rule.title': title } }; }, isInstallable(): QueryDslQueryContainer[] { - return [this.isFullyTranslated(), this.isNotInstalled()]; + return [conditions.isFullyTranslated(), conditions.isNotInstalled()]; }, isNotInstallable(): QueryDslQueryContainer[] { - return [this.isNotFullyTranslated(), this.isInstalled()]; + return [conditions.isNotFullyTranslated(), conditions.isInstalled()]; }, isFailed(): QueryDslQueryContainer { return { term: { status: SiemMigrationStatus.FAILED } }; From 70b4cff57dcfe5d293b5035d5bc69a3ae61a78a0 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Mon, 20 Jan 2025 17:41:14 +0100 Subject: [PATCH 05/25] check capabilities on the onboarding page --- .../siem_migrations/product_feature_config.ts | 2 +- .../common/siem_migrations/constants.ts | 3 ++ .../lib/capabilities/has_capabilities.ts | 13 +++++- .../public/common/lib/capabilities/index.ts | 6 ++- .../siem_migrations/start_migration/index.ts | 9 +++-- .../start_migration_check_complete.ts | 40 +++++++++++++++---- .../start_migration/translations.ts | 19 +++++++++ .../lib/siem_migrations/rules/api/index.ts | 3 ++ .../common/pli/pli_config.ts | 1 + 9 files changed, 82 insertions(+), 14 deletions(-) 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 index 8e7ce9812fab4..d74184fa3db4d 100644 --- 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 @@ -27,7 +27,7 @@ export const siemMigrationsDefaultProductFeaturesConfig: Record< privileges: { all: { api: [SIEM_MIGRATIONS_FEATURE_ID], - ui: ['siem-migrations'], + ui: ['all'], }, }, }, 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 d9ff4afd9a214..ca1d8d2b5840a 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,9 @@ 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_PRIVILEGES_PATH = + `${SIEM_RULE_MIGRATIONS_PATH}/privileges` as const; + export enum SiemMigrationTaskStatus { READY = 'ready', RUNNING = 'running', 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..28d4bee743052 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'; /** @@ -41,3 +42,13 @@ export const hasCapabilities = ( }); } }; + +/** + * A 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/index.ts b/x-pack/solutions/security/plugins/security_solution/public/common/lib/capabilities/index.ts index 22bc4e2d4fae4..db024b2e3941b 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 @@ -5,4 +5,8 @@ * 2.0. */ -export { hasCapabilities, type RequiredCapabilities } from './has_capabilities'; +export { + hasCapabilities, + CapabilitiesChecker, + type RequiredCapabilities, +} from './has_capabilities'; 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..8c99057aec3ed 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,12 @@ 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 { checkStartMigrationCardComplete } from './start_migration_check_complete'; export const startMigrationCardConfig: OnboardingCardConfig = { id: OnboardingCardId.siemMigrationsStart, title: START_MIGRATION_CARD_TITLE, icon: cardIcon, + licenseTypeRequired: 'enterprise', Component: React.lazy( () => import( @@ -23,6 +23,9 @@ export const startMigrationCardConfig: OnboardingCardConfig = { './start_migration_card' ) ), - checkComplete: checkStartMigrationCardComplete, - licenseTypeRequired: 'enterprise', + checkComplete: (services) => + import( + /* webpackChunkName: "onboarding_siem_migrations_start_migration_card_check_complete" */ + './start_migration_check_complete' + ).then((module) => module.checkStartMigrationCardComplete(services)), }; 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..96e54d5181d76 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 @@ -5,15 +5,39 @@ * 2.0. */ +import { SIEM_MIGRATIONS_FEATURE_ID } from '@kbn/security-solution-features/constants'; +import { CapabilitiesChecker } from '../../../../../../common/lib/capabilities'; +// import { getUserPrivilege as getAlertsUserPrivilege } from '../../../../../../detections/containers/detection_engine/alerts/api'; import { SiemMigrationTaskStatus } from '../../../../../../../common/siem_migrations/constants'; + import type { OnboardingCardCheckComplete } from '../../../../../types'; +import type { StartMigrationCardMetadata } from './types'; +import { CAPABILITIES_REQUIRED } from './translations'; + +export const checkStartMigrationCardComplete: OnboardingCardCheckComplete< + StartMigrationCardMetadata +> = async ({ siemMigrations, application }) => { + const capabilities = new CapabilitiesChecker(application.capabilities); + + const missingCapabilities: string[] = []; + if (!capabilities.has('siem.all')) { + missingCapabilities.push(CAPABILITIES_REQUIRED.securityAll); + } + if (!capabilities.has(`${SIEM_MIGRATIONS_FEATURE_ID}.all`)) { + missingCapabilities.push(CAPABILITIES_REQUIRED.siemMigrationsAll); + } + if (!capabilities.has('actions.execute')) { + missingCapabilities.push(CAPABILITIES_REQUIRED.connectorsRead); + } + + 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, missingIndexPrivileges: [] } }; }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts index b5c15b00e20e6..3f40f0711b7c1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts @@ -56,3 +56,22 @@ export const START_MIGRATION_CARD_UPLOAD_MORE_BUTTON = i18n.translate( 'xpack.securitySolution.onboarding.startMigration.uploadMore.button', { defaultMessage: 'Upload more rules' } ); + +export const CAPABILITIES_REQUIRED = { + siemMigrationsAll: i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.capability.siemMigrationsAll', + { defaultMessage: 'Security > SIEM migrations: All' } + ), + securityAll: i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.capability.securityAll', + { defaultMessage: 'Security > Security : All' } + ), + connectorsRead: i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.capability.connectorsRead', + { defaultMessage: 'Management > Actions and Connectors : Read' } + ), + fleetRead: i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.capability.fleetRead', + { defaultMessage: 'Management > Fleet : Read' } + ), +}; 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 24d33ea27f23c..e62f81e1f19d3 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 @@ -22,6 +22,7 @@ import { registerSiemRuleMigrationsInstallTranslatedRoute } from './install_tran import { registerSiemRuleMigrationsResourceGetMissingRoute } from './resources/missing'; import { registerSiemRuleMigrationsPrebuiltRulesRoute } from './get_prebuilt_rules'; import { registerSiemRuleMigrationsIntegrationsRoute } from './get_integrations'; +import { registerSiemRuleMigrationsGetPrivilegesRoute } from './privileges/get_privileges'; export const registerSiemRuleMigrationsRoutes = ( router: SecuritySolutionPluginRouter, @@ -43,4 +44,6 @@ export const registerSiemRuleMigrationsRoutes = ( registerSiemRuleMigrationsResourceUpsertRoute(router, logger); registerSiemRuleMigrationsResourceGetRoute(router, logger); registerSiemRuleMigrationsResourceGetMissingRoute(router, logger); + + registerSiemRuleMigrationsGetPrivilegesRoute(router, logger); }; 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 4bb78efb4b701..49aac4f6d05ea 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 @@ -29,6 +29,7 @@ export const PLI_PRODUCT_FEATURES: PliProductFeatures = { ProductFeatureKey.casesConnectors, ProductFeatureKey.externalRuleActions, ProductFeatureKey.integrationAssistant, + ProductFeatureKey.siemMigrations, ], }, endpoint: { From c72b2dce57f01b07fe8686b6d1dc754e1ce97e5c Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Mon, 20 Jan 2025 17:42:35 +0100 Subject: [PATCH 06/25] siem migrations privileges endpoint --- .../security/packages/features/constants.ts | 7 ++ .../siem_migrations/start_migration/types.ts | 11 +++ .../rules/api/privileges/get_privileges.ts | 79 +++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 x-pack/solutions/security/packages/features/constants.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/types.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/privileges/get_privileges.ts 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/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..63228b976af8f --- /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,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface StartMigrationCardMetadata { + missingCapabilities: string[]; + missingIndexPrivileges: Array<{ index: string; privilege: string }>; +} diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/privileges/get_privileges.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/privileges/get_privileges.ts new file mode 100644 index 0000000000000..91e4a62dbe4e9 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/privileges/get_privileges.ts @@ -0,0 +1,79 @@ +/* + * 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 { SIEM_RULE_MIGRATION_PRIVILEGES_PATH } from '../../../../../../common/siem_migrations/constants'; +import type { SecuritySolutionPluginRouter } from '../../../../../types'; + +export const registerSiemRuleMigrationsGetPrivilegesRoute = ( + router: SecuritySolutionPluginRouter, + logger: Logger +) => { + router.versioned + .get({ + path: SIEM_RULE_MIGRATION_PRIVILEGES_PATH, + access: 'internal', + security: { authz: { requiredPrivileges: ['securitySolution'] } }, + }) + .addVersion( + { version: '1', validate: false }, + 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 spaceId = securitySolution.getSpaceId(); + const clusterPrivileges = await readPrivileges(esClient, `lookup_*`); + + const privileges = { + ...(clusterPrivileges as object), + is_authenticated: request.auth.isAuthenticated ?? false, + }; + + return response.ok({ body: privileges }); + } catch (err) { + logger.error(err); + return response.badRequest({ body: err.message }); + } + } + ); +}; + +const readPrivileges = async ( + esClient: ElasticsearchClient, + index: string +): Promise => { + const response = await esClient.security.hasPrivileges( + { + body: { + index: [ + { + names: [index], + privileges: [ + 'read', + 'write', + 'view_index_metadata', + 'manage', + 'create_doc', + 'create_index', + 'index', + ], + }, + ], + }, + }, + { meta: true } + ); + return response.body; +}; From 0e2cf9bb0c102e63b3e1803996a93bdf2a68eaff Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Fri, 24 Jan 2025 12:01:21 +0100 Subject: [PATCH 07/25] missing privielges callout --- .../common/siem_migrations/constants.ts | 2 + .../cards/assistant/assistant_card.tsx | 4 +- .../common/connectors/connector_cards.tsx | 10 +--- .../connectors/create_connector_popover.tsx | 7 ++- .../common/connectors/missing_privileges.tsx | 58 +++++++------------ .../cards/common/connectors/translations.ts | 27 ++------- .../cards/common/missing_privileges/index.ts | 12 ++++ .../missing_privileges/missing_privileges.tsx | 57 ++++++++++++++++++ .../common/missing_privileges/translations.ts | 29 ++++++++++ .../ai_connector/ai_connector_card.tsx | 4 +- .../start_migration/start_migration_card.tsx | 20 ++++++- .../start_migration_check_complete.ts | 2 +- .../siem_migrations/start_migration/types.ts | 1 - .../rules/api/privileges/get_privileges.ts | 8 ++- .../rule_migrations_data_lookups_client.ts | 3 +- 15 files changed, 162 insertions(+), 82 deletions(-) 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 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 70ecbbc53d25f..d2a3dabf08e38 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 @@ -32,6 +32,8 @@ export const SIEM_RULE_MIGRATION_RESOURCES_MISSING_PATH = export const SIEM_RULE_MIGRATION_PRIVILEGES_PATH = `${SIEM_RULE_MIGRATIONS_PATH}/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/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 c971bd2a6f0b7..6ea2018901af5 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 @@ -11,12 +11,12 @@ import { css } from '@emotion/css'; import { OnboardingCardId } from '../../../../constants'; import type { OnboardingCardComponent } from '../../../../types'; import * as i18n from './translations'; +import { ConnectorsMissingPrivilegesCallOut } from '../common/connectors/missing_privileges'; 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'; export const AssistantCard: OnboardingCardComponent = ({ isCardComplete, @@ -83,7 +83,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 b8b51198c75ff..1375ba9511afc 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 @@ -15,7 +15,6 @@ import { EuiText, EuiBadge, EuiSpacer, - EuiCallOut, useEuiTheme, } from '@elastic/eui'; import { css } from '@emotion/css'; @@ -25,8 +24,7 @@ import { type CreateConnectorPopoverProps, } from './create_connector_popover'; import { ConnectorSetup } from './connector_setup'; -import * as i18n from './translations'; -import { MissingPrivilegesDescription } from './missing_privileges'; +import { ConnectorsMissingPrivilegesCallOut } from './missing_privileges'; interface ConnectorCardsProps extends CreateConnectorPopoverProps, @@ -50,11 +48,7 @@ export const ConnectorCards = React.memo( // show callout when user is missing actions.save privilege if (!hasConnectors && !canCreateConnectors) { - return ( - - - - ); + return ; } return ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/create_connector_popover.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/create_connector_popover.tsx index c6c378fc8e29f..5397c9659bd65 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/create_connector_popover.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/create_connector_popover.tsx @@ -9,7 +9,8 @@ import { css } from '@emotion/css'; import { EuiPopover, EuiLink, EuiText } from '@elastic/eui'; import { ConnectorSetup } from './connector_setup'; import * as i18n from './translations'; -import { MissingPrivilegesTooltip } from './missing_privileges'; +import { ConnectorsMissingPrivilegesDescription } from './missing_privileges'; +import { MissingPrivilegesTooltip } from '../missing_privileges'; export interface CreateConnectorPopoverProps { onConnectorSaved: () => void; @@ -27,7 +28,9 @@ export const CreateConnectorPopover = React.memo( ); if (!canCreateConnectors) { return ( - + } + > {i18n.ASSISTANT_CARD_CREATE_NEW_CONNECTOR_POPOVER} 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..31b004b1d5971 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,27 @@ * 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/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 983a6a67f5b32..08d1774ffa204 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 @@ -14,30 +14,11 @@ export const ASSISTANT_CARD_CREATE_NEW_CONNECTOR_POPOVER = i18n.translate( } ); -export const PRIVILEGES_MISSING_TITLE = i18n.translate( - 'xpack.securitySolution.onboarding.assistantCard.missingPrivileges.title', - { - defaultMessage: 'Missing privileges', - } +export const REQUIRED_PRIVILEGES_CONNECTORS_READ = i18n.translate( + 'xpack.securitySolution.onboarding.assistantCard.requiredPrivileges.connectorsRead', + { defaultMessage: 'Management > Actions & Connectors: Read' } ); - -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_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..4a5f515ea2199 --- /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,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { PropsWithChildren } from 'react'; +import React from 'react'; +import { EuiCallOut, EuiCode, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +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 }) => { + 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 1786c9cbee85c..143e999b94813 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 @@ -16,7 +16,7 @@ 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'; export const AIConnectorCard: OnboardingCardComponent = ({ checkCompleteMetadata, @@ -64,7 +64,7 @@ export const AIConnectorCard: OnboardingCardComponent = ) : ( - + )} ); 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..9034af60a2926 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,13 @@ 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 { 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 +65,22 @@ 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 96e54d5181d76..2e67be096cab6 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 @@ -39,5 +39,5 @@ export const checkStartMigrationCardComplete: OnboardingCardCheckComplete< ); } - return { isComplete, metadata: { missingCapabilities, missingIndexPrivileges: [] } }; + 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 index 63228b976af8f..065fa1df66e9d 100644 --- 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 @@ -7,5 +7,4 @@ export interface StartMigrationCardMetadata { missingCapabilities: string[]; - missingIndexPrivileges: Array<{ index: string; privilege: string }>; } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/privileges/get_privileges.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/privileges/get_privileges.ts index 91e4a62dbe4e9..398e04f9ce744 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/privileges/get_privileges.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/privileges/get_privileges.ts @@ -7,7 +7,10 @@ import type { ElasticsearchClient, IKibanaResponse, Logger } from '@kbn/core/server'; import type { SecurityHasPrivilegesResponse } from '@elastic/elasticsearch/lib/api/types'; -import { SIEM_RULE_MIGRATION_PRIVILEGES_PATH } from '../../../../../../common/siem_migrations/constants'; +import { + SIEM_RULE_MIGRATION_PRIVILEGES_PATH, + LOOKUPS_INDEX_PREFIX, +} from '../../../../../../common/siem_migrations/constants'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; export const registerSiemRuleMigrationsGetPrivilegesRoute = ( @@ -33,8 +36,7 @@ export const registerSiemRuleMigrationsGetPrivilegesRoute = ( return response.notFound(); } - // const spaceId = securitySolution.getSpaceId(); - const clusterPrivileges = await readPrivileges(esClient, `lookup_*`); + const clusterPrivileges = await readPrivileges(esClient, `${LOOKUPS_INDEX_PREFIX}*`); const privileges = { ...(clusterPrivileges as object), 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({ From ad6804c54bd4aeadc0f7006f9895e025651a697c Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Fri, 24 Jan 2025 12:13:57 +0100 Subject: [PATCH 08/25] add callout --- .../start_migration/start_migration_card.tsx | 13 +++++++++++-- .../start_migration/translations.ts | 16 ++++++---------- 2 files changed, 17 insertions(+), 12 deletions(-) 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 9034af60a2926..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 @@ -19,7 +19,10 @@ import type { StartMigrationCardMetadata } from './types'; import { RuleMigrationsPanels } from './rule_migrations_panels'; import { useStyles } from './start_migration_card.styles'; import * as i18n from './translations'; -import { MissingPrivilegesDescription } from '../../common/missing_privileges'; +import { + MissingPrivilegesCallOut, + MissingPrivilegesDescription, +} from '../../common/missing_privileges'; const StartMigrationsBody: OnboardingCardComponent = React.memo( ({ setComplete, isCardComplete, setExpandedCardId }) => { @@ -75,7 +78,13 @@ export const StartMigrationCard: OnboardingCardComponent 0) { - return ; + return ( + + + + + + ); } return ; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts index 3f40f0711b7c1..86cba6f1affb1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts @@ -58,20 +58,16 @@ export const START_MIGRATION_CARD_UPLOAD_MORE_BUTTON = i18n.translate( ); export const CAPABILITIES_REQUIRED = { - siemMigrationsAll: i18n.translate( - 'xpack.securitySolution.onboarding.startMigration.capability.siemMigrationsAll', - { defaultMessage: 'Security > SIEM migrations: All' } - ), securityAll: i18n.translate( 'xpack.securitySolution.onboarding.startMigration.capability.securityAll', - { defaultMessage: 'Security > Security : All' } + { defaultMessage: 'Security > Security: All' } + ), + siemMigrationsAll: i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.capability.siemMigrationsAll', + { defaultMessage: 'Security > Security > SIEM migrations: All' } ), connectorsRead: i18n.translate( 'xpack.securitySolution.onboarding.startMigration.capability.connectorsRead', - { defaultMessage: 'Management > Actions and Connectors : Read' } - ), - fleetRead: i18n.translate( - 'xpack.securitySolution.onboarding.startMigration.capability.fleetRead', - { defaultMessage: 'Management > Fleet : Read' } + { defaultMessage: 'Management > Actions and Connectors: Read' } ), }; From 273737a6e5530908ad621cca11986599e415efb3 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Fri, 24 Jan 2025 17:51:12 +0100 Subject: [PATCH 09/25] fix connector selector --- .../common/connectors/connector_cards.tsx | 18 +- .../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 | 8 +- .../ai_connector/ai_connector_card.tsx | 4 +- .../siem_migrations/start_migration/index.ts | 7 +- 7 files changed, 171 insertions(+), 232 deletions(-) delete mode 100644 x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_selector_with_icon.tsx 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 cc07e31dbb958..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,19 +5,22 @@ * 2.0. */ -import React, { useCallback } from 'react'; +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 { 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; } @@ -29,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); @@ -38,7 +48,7 @@ export const ConnectorCards = React.memo( [onConnectorSelected, onNewConnectorSaved] ); - if (!connectors) { + if (!connectors || !actionTypes) { return ; } @@ -66,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 index 5c1c18df71a45..b1ecf0a26e002 100644 --- 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 @@ -6,12 +6,12 @@ */ 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 { useLoadActionTypes } from '@kbn/elastic-assistant/impl/connectorland/use_load_action_types'; +import { useKibana } from '../../../../../../../common/lib/kibana/kibana_react'; import { AIActionTypeIds } from '../constants'; export const useFilteredActionTypes = () => { - const { http, toasts } = useAssistantContext(); - const { data } = loadActionTypes({ http, toasts }); + const { http, notifications } = useKibana().services; + const { data } = useLoadActionTypes({ http, toasts: notifications.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/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 02f3f3ce465de..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 @@ -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) => { 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 5158c1674ff37..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 @@ -11,6 +11,7 @@ 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 = { id: OnboardingCardId.siemMigrationsStart, @@ -24,9 +25,5 @@ export const startMigrationCardConfig: OnboardingCardConfig - import( - /* webpackChunkName: "onboarding_siem_migrations_start_migration_card_check_complete" */ - './start_migration_check_complete' - ).then((module) => module.checkStartMigrationCardComplete(services)), + checkComplete: checkStartMigrationCardComplete, }; From 1e5356ccfafa579595ca6fd8b53c843926036809 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Fri, 24 Jan 2025 17:51:53 +0100 Subject: [PATCH 10/25] remove unused hook --- .../connectors/hooks/use_load_action_types.ts | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/hooks/use_load_action_types.ts 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 b1ecf0a26e002..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 } from '@kbn/elastic-assistant/impl/connectorland/use_load_action_types'; -import { useKibana } from '../../../../../../../common/lib/kibana/kibana_react'; -import { AIActionTypeIds } from '../constants'; - -export const useFilteredActionTypes = () => { - const { http, notifications } = useKibana().services; - const { data } = useLoadActionTypes({ http, toasts: notifications.toasts }); - return useMemo(() => data?.filter(({ id }) => AIActionTypeIds.includes(id)), [data]); -}; From 12f6b74ee08c602324c6ff32d7476bf9feabfd16 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Mon, 27 Jan 2025 20:08:42 +0100 Subject: [PATCH 11/25] add rbac to rules table --- .../side_nav/src/solution_side_nav_panel.tsx | 4 +- .../common/siem_migrations/constants.ts | 4 +- .../model/api/rules/rule_migration.gen.ts | 17 ++++ .../api/rules/rule_migration.schema.yaml | 32 +++++++ .../lib/capabilities/capabilities_checker.ts | 19 +++++ .../lib/capabilities/has_capabilities.ts | 10 --- .../public/common/lib/capabilities/index.ts | 7 +- .../ai_connector/connectors_check_complete.ts | 2 +- .../start_migration_check_complete.ts | 23 +---- .../start_migration/translations.ts | 15 ---- .../public/siem_migrations/links.ts | 8 +- .../public/siem_migrations/rules/api/index.ts | 16 ++++ .../logic/use_get_migration_privileges.ts | 41 +++++++++ .../siem_migrations/rules/pages/index.tsx | 43 +++++----- .../pages/missing_privileges_callout.tsx | 72 ++++++++++++++++ .../rules/service/capabilities.ts | 69 +++++++++++++++ .../rules/service/rule_migrations_service.ts | 24 +++++- .../lib/siem_migrations/rules/api/index.ts | 4 +- .../api/privileges/get_missing_privileges.ts | 78 +++++++++++++++++ .../rules/api/privileges/get_privileges.ts | 83 ------------------- .../lib/siem_migrations/rules/api/start.ts | 6 ++ 21 files changed, 410 insertions(+), 167 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/common/lib/capabilities/capabilities_checker.ts 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/server/lib/siem_migrations/rules/api/privileges/get_missing_privileges.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/privileges/get_privileges.ts 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/siem_migrations/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts index 401a7c29118e6..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,8 +31,8 @@ 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_PRIVILEGES_PATH = - `${SIEM_RULE_MIGRATIONS_PATH}/privileges` as const; +export const SIEM_RULE_MIGRATION_MISSING_PRIVILEGES_PATH = + `${SIEM_RULE_MIGRATIONS_PATH}/missing_privileges` as const; export const LOOKUPS_INDEX_PREFIX = 'lookup_'; 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..1fb33b1e0ccd3 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 @@ -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 >; 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..6d5a16b2abde9 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 @@ -535,3 +535,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 28d4bee743052..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 @@ -42,13 +42,3 @@ export const hasCapabilities = ( }); } }; - -/** - * A 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/index.ts b/x-pack/solutions/security/plugins/security_solution/public/common/lib/capabilities/index.ts index db024b2e3941b..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 @@ -5,8 +5,5 @@ * 2.0. */ -export { - hasCapabilities, - CapabilitiesChecker, - type RequiredCapabilities, -} from './has_capabilities'; +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/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 00345fd14d28a..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,7 +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'; +import { CapabilitiesChecker } from '../../../../../../common/lib/capabilities/capabilities_checker'; import type { OnboardingCardCheckComplete } from '../../../../../types'; import { AIActionTypeIds } from '../../common/connectors/constants'; import type { AIConnectorCardMetadata } from './types'; 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 6b7b7a9ce1d83..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 @@ -5,32 +5,17 @@ * 2.0. */ -import { - SECURITY_FEATURE_ID_V2, - SIEM_MIGRATIONS_FEATURE_ID, -} from '@kbn/security-solution-features/constants'; -import { CapabilitiesChecker } from '../../../../../../common/lib/capabilities'; import { SiemMigrationTaskStatus } from '../../../../../../../common/siem_migrations/constants'; import type { OnboardingCardCheckComplete } from '../../../../../types'; import type { StartMigrationCardMetadata } from './types'; -import { CAPABILITIES_REQUIRED } from './translations'; export const checkStartMigrationCardComplete: OnboardingCardCheckComplete< StartMigrationCardMetadata -> = async ({ siemMigrations, application }) => { - const capabilities = new CapabilitiesChecker(application.capabilities); - - const missingCapabilities: string[] = []; - if (!capabilities.has(`${SECURITY_FEATURE_ID_V2}.crud`)) { - missingCapabilities.push(CAPABILITIES_REQUIRED.securityAll); - } - if (!capabilities.has(`${SIEM_MIGRATIONS_FEATURE_ID}.all`)) { - missingCapabilities.push(CAPABILITIES_REQUIRED.siemMigrationsAll); - } - if (!capabilities.has('actions.execute')) { - missingCapabilities.push(CAPABILITIES_REQUIRED.connectorsRead); - } +> = async ({ siemMigrations }) => { + const missingCapabilities = siemMigrations.rules + .getMissingCapabilities('all') + .map(({ description }) => description); let isComplete = false; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts index 86cba6f1affb1..b5c15b00e20e6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts @@ -56,18 +56,3 @@ export const START_MIGRATION_CARD_UPLOAD_MORE_BUTTON = i18n.translate( 'xpack.securitySolution.onboarding.startMigration.uploadMore.button', { defaultMessage: 'Upload more rules' } ); - -export const CAPABILITIES_REQUIRED = { - securityAll: i18n.translate( - 'xpack.securitySolution.onboarding.startMigration.capability.securityAll', - { defaultMessage: 'Security > Security: All' } - ), - siemMigrationsAll: i18n.translate( - 'xpack.securitySolution.onboarding.startMigration.capability.siemMigrationsAll', - { defaultMessage: 'Security > Security > SIEM migrations: All' } - ), - connectorsRead: i18n.translate( - 'xpack.securitySolution.onboarding.startMigration.capability.connectorsRead', - { defaultMessage: 'Management > Actions and Connectors: Read' } - ), -}; 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 ede76286696cf..fb86032cb46c9 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 = { ], experimentalKey: 'siemMigrationsEnabled', 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..49d16fe164d8e 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; 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/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..7dc97e38dbe7f --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/capabilities.ts @@ -0,0 +1,69 @@ +/* + * 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 > Security > SIEM migrations: All' } + ), + }, +]; + +const allCapabilities: MissingCapability[] = [ + ...minimumCapabilities, + { + capability: `${SECURITY_FEATURE_ID_V2}.crud`, + description: i18n.translate( + 'xpack.securitySolution.siemMigrations.service.capabilities.securityAll', + { defaultMessage: 'Security > Security: 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/rule_migrations_service.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts index 60a09b9139aff..bbebc8f8d2c12 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,6 +39,11 @@ import { upsertMigrationResources, getIntegrations, } from '../api'; +import { + getMissingCapabilities, + type MissingCapability, + type CapabilitiesLevel, +} from './capabilities'; import type { RuleMigrationStats } from '../types'; import { getSuccessToast } from './success_notification'; import { RuleMigrationsStorage } from './storage'; @@ -76,8 +81,20 @@ 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().siemMigrationsEnabled && licenseService.isEnterprise(); + return ( + ExperimentalFeaturesService.get().siemMigrationsEnabled && + licenseService.isEnterprise() && + !this.hasMissingCapabilities('minimum') + ); } public startPolling() { @@ -174,8 +191,9 @@ export class SiemRulesMigrationsService { } return getRuleMigrationsStatsAll(params).catch((e) => { - // Retry only on network errors (no e.status) and 503 (Service Unavailable), otherwise throw - if (e.response.status || (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 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 d29dadcb07111..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,7 +21,7 @@ import { registerSiemRuleMigrationsInstallRoute } from './install'; import { registerSiemRuleMigrationsResourceGetMissingRoute } from './resources/missing'; import { registerSiemRuleMigrationsPrebuiltRulesRoute } from './get_prebuilt_rules'; import { registerSiemRuleMigrationsIntegrationsRoute } from './get_integrations'; -import { registerSiemRuleMigrationsGetPrivilegesRoute } from './privileges/get_privileges'; +import { registerSiemRuleMigrationsGetMissingPrivilegesRoute } from './privileges/get_missing_privileges'; export const registerSiemRuleMigrationsRoutes = ( router: SecuritySolutionPluginRouter, @@ -43,5 +43,5 @@ export const registerSiemRuleMigrationsRoutes = ( registerSiemRuleMigrationsResourceGetRoute(router, logger); registerSiemRuleMigrationsResourceGetMissingRoute(router, logger); - registerSiemRuleMigrationsGetPrivilegesRoute(router, logger); + registerSiemRuleMigrationsGetMissingPrivilegesRoute(router, logger); }; 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/privileges/get_privileges.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/privileges/get_privileges.ts deleted file mode 100644 index 663ad79a07c7b..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/privileges/get_privileges.ts +++ /dev/null @@ -1,83 +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 { ElasticsearchClient, IKibanaResponse, Logger } from '@kbn/core/server'; -import type { SecurityHasPrivilegesResponse } from '@elastic/elasticsearch/lib/api/types'; -import { - SIEM_RULE_MIGRATION_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 registerSiemRuleMigrationsGetPrivilegesRoute = ( - router: SecuritySolutionPluginRouter, - logger: Logger -) => { - router.versioned - .get({ - path: SIEM_RULE_MIGRATION_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 clusterPrivileges = await readPrivileges(esClient, `${LOOKUPS_INDEX_PREFIX}*`); - - const privileges = { - ...(clusterPrivileges as object), - is_authenticated: request.auth.isAuthenticated ?? false, - }; - - return response.ok({ body: privileges }); - } catch (err) { - logger.error(err); - return response.badRequest({ body: err.message }); - } - }) - ); -}; - -const readPrivileges = async ( - esClient: ElasticsearchClient, - index: string -): Promise => { - const response = await esClient.security.hasPrivileges( - { - body: { - index: [ - { - names: [index], - privileges: [ - 'read', - 'write', - 'view_index_metadata', - 'manage', - 'create_doc', - 'create_index', - 'index', - ], - }, - ], - }, - }, - { meta: true } - ); - return response.body; -}; 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 df23ef138890b..e2efb190cc540 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 @@ -54,6 +54,12 @@ export const registerSiemRuleMigrationsStartRoute = ( try { const ctx = await context.resolve(['core', 'actions', 'alerting', 'securitySolution']); + // 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) { From 1df73339fa9b2f3c3442a3a10bf74ed89471fcc6 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Tue, 28 Jan 2025 14:22:44 +0100 Subject: [PATCH 12/25] add start migration controls --- .../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 | 14 +++- .../rules/service/translations.ts | 5 -- 6 files changed, 136 insertions(+), 10 deletions(-) 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%) 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 bbebc8f8d2c12..612580164d7a2 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 @@ -45,9 +45,11 @@ import { 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 = @@ -143,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(); 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' } From 21101819929a4851589191098e63dddaf02967d0 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Tue, 28 Jan 2025 18:58:59 +0100 Subject: [PATCH 13/25] not start migrations automatically if some capability is missing --- .../siem_migrations/rules/service/capabilities.ts | 10 ++++++++-- .../rules/service/rule_migrations_service.ts | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) 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 index 7dc97e38dbe7f..fccbceb3f1646 100644 --- 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 @@ -30,13 +30,12 @@ const minimumCapabilities: MissingCapability[] = [ capability: `${SIEM_MIGRATIONS_FEATURE_ID}.all`, description: i18n.translate( 'xpack.securitySolution.siemMigrations.service.capabilities.siemMigrationsAll', - { defaultMessage: 'Security > Security > SIEM migrations: All' } + { defaultMessage: 'Security > SIEM migrations: All' } ), }, ]; const allCapabilities: MissingCapability[] = [ - ...minimumCapabilities, { capability: `${SECURITY_FEATURE_ID_V2}.crud`, description: i18n.translate( @@ -44,6 +43,13 @@ const allCapabilities: MissingCapability[] = [ { 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( 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 612580164d7a2..28119b01b5405 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 @@ -243,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); From 5633a961dad8322d1af2ac903b721f72c16d4911 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Wed, 29 Jan 2025 18:15:04 +0100 Subject: [PATCH 14/25] hide onboarding topic when feature missing --- .../plugins/security_solution/public/onboarding/config.ts | 2 ++ 1 file changed, 2 insertions(+) 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', }, ]; From 0408ae19024184558f0f067185edcd931456cd14 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 29 Jan 2025 17:40:36 +0000 Subject: [PATCH 15/25] [CI] Auto-commit changed files from 'yarn openapi:generate' --- .../common/api/quickstart_client.gen.ts | 16 ++++++++++++++++ .../services/security_solution_api.gen.ts | 10 ++++++++++ 2 files changed, 26 insertions(+) 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 a57be4b8f0680..c3d4595406625 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, @@ -1491,6 +1492,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 */ 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 a069b2e1134ce..ba51e74e551a3 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 @@ -1005,6 +1005,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 */ From e759883223c0659d85baf870bb4b1fb6ecff4749 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Thu, 30 Jan 2025 15:49:49 +0100 Subject: [PATCH 16/25] fix test --- .../product_features_service.test.ts | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) 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 c8bff292ac6c2..133ed30e2bd29 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 @@ -61,8 +61,8 @@ describe('ProductFeaturesService', () => { const experimentalFeatures = {} as ExperimentalFeatures; new ProductFeaturesService(loggerMock.create(), experimentalFeatures); - expect(mockGetFeature).toHaveBeenCalledTimes(8); - expect(MockedProductFeatures).toHaveBeenCalledTimes(8); + expect(mockGetFeature).toHaveBeenCalledTimes(9); + expect(MockedProductFeatures).toHaveBeenCalledTimes(9); }); it('should init all ProductFeatures when initialized', () => { @@ -94,24 +94,18 @@ describe('ProductFeaturesService', () => { const mockCasesConfig = new Map() as ProductFeaturesConfig; const mockAssistantConfig = new Map() as ProductFeaturesConfig; const mockAttackDiscoveryConfig = new Map() as ProductFeaturesConfig; -<<<<<<< HEAD const mockSiemMigrationsConfig = new Map() as ProductFeaturesConfig; -======= const mockTimelineConfig = new Map() as ProductFeaturesConfig; const mockNotesConfig = new Map() as ProductFeaturesConfig; ->>>>>>> upstream/main const configurator: ProductFeaturesConfigurator = { security: jest.fn(() => mockSecurityConfig), cases: jest.fn(() => mockCasesConfig), securityAssistant: jest.fn(() => mockAssistantConfig), -<<<<<<< HEAD attackDiscovery: jest.fn(() => mockAttackDiscoveryConfig), siemMigrations: jest.fn(() => mockSiemMigrationsConfig), -======= timeline: jest.fn(() => mockTimelineConfig), notes: jest.fn(() => mockNotesConfig), ->>>>>>> upstream/main }; productFeaturesService.setProductFeaturesConfigurator(configurator); @@ -159,26 +153,20 @@ describe('ProductFeaturesService', () => { const mockAttackDiscoveryConfig = new Map([ [ProductFeatureKey.attackDiscovery, {}], ]) as ProductFeaturesConfig; -<<<<<<< HEAD const mockSiemMigrationsConfig = new Map([ [ProductFeatureKey.siemMigrations, {}], ]) as ProductFeaturesConfig; -======= const mockTimelineConfig = new Map([[ProductFeatureKey.timeline, {}]]) as ProductFeaturesConfig; const mockNotesConfig = new Map([[ProductFeatureKey.notes, {}]]) as ProductFeaturesConfig; ->>>>>>> upstream/main const configurator: ProductFeaturesConfigurator = { security: jest.fn(() => mockSecurityConfig), cases: jest.fn(() => mockCasesConfig), securityAssistant: jest.fn(() => mockAssistantConfig), -<<<<<<< HEAD attackDiscovery: jest.fn(() => mockAttackDiscoveryConfig), siemMigrations: jest.fn(() => mockSiemMigrationsConfig), -======= timeline: jest.fn(() => mockTimelineConfig), notes: jest.fn(() => mockNotesConfig), ->>>>>>> upstream/main }; productFeaturesService.setProductFeaturesConfigurator(configurator); From b1b1b4f2460ff7c965ad01ff89a09bb60bbb4787 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Thu, 30 Jan 2025 16:52:54 +0100 Subject: [PATCH 17/25] fix types --- .../components/hooks/use_stored_state.ts | 5 ++++- .../server/lib/product_features_service/mocks.ts | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) 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/server/lib/product_features_service/mocks.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/mocks.ts index 3b385c70e0b5f..bb88527760569 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 @@ -181,6 +181,21 @@ export const createProductFeaturesServiceMock = ( ]) ) ), + siemMigrations: jest.fn().mockReturnValue( + new Map( + enabledFeatureKeys.map((key) => [ + key, + { + privileges: { + all: { + api: ['test-api-action'], + ui: ['test-ui-action'], + }, + }, + }, + ]) + ) + ), }); } From 62d668f406986e8f8da1a281def0bdc580c61d2f Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Fri, 31 Jan 2025 14:41:53 +0100 Subject: [PATCH 18/25] disable SIEM migrations feature with feature flag --- .../product_features_service.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) 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 d7b819b4d2023..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,12 +4,6 @@ * 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'; @@ -203,8 +197,11 @@ export class ProductFeaturesService { const notesProductFeaturesConfig = configurator.notes(); this.notesProductFeatures.setConfig(notesProductFeaturesConfig); - const siemMigrationsProductFeaturesConfig = configurator.siemMigrations(); - this.siemMigrationsProductFeatures.setConfig(siemMigrationsProductFeaturesConfig); + let siemMigrationsProductFeaturesConfig = new Map(); + if (!this.experimentalFeatures.siemMigrationsDisabled) { + siemMigrationsProductFeaturesConfig = configurator.siemMigrations(); + this.siemMigrationsProductFeatures.setConfig(siemMigrationsProductFeaturesConfig); + } this.productFeatures = new Set( Object.freeze([ From 58c5eb76c5e8f3242305e7e410b1ea5ce9405d28 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Fri, 31 Jan 2025 16:55:45 +0100 Subject: [PATCH 19/25] fix more tests --- .../packages/side_nav/src/solution_side_nav_panel.test.tsx | 2 -- .../product_features_service/product_features_service.test.ts | 4 ++-- .../test/api_integration/apis/features/features/features.ts | 2 ++ x-pack/test/api_integration/apis/security/privileges.ts | 1 + x-pack/test/api_integration/apis/security/privileges_basic.ts | 2 ++ .../spaces_api_integration/common/suites/create.agnostic.ts | 1 + .../spaces_api_integration/spaces_only/telemetry/telemetry.ts | 1 + .../ui_capabilities/security_and_spaces/tests/nav_links.ts | 1 + 8 files changed, 10 insertions(+), 4 deletions(-) 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..f3c17f2c0b629 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'; @@ -98,7 +97,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/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 5f8453174eb53..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 @@ -62,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', () => { 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/spaces_api_integration/common/suites/create.agnostic.ts b/x-pack/test/spaces_api_integration/common/suites/create.agnostic.ts index 58d0b5c5d5380..d0ae38050a103 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 @@ -98,6 +98,7 @@ export function createTestSuiteFactory({ getService }: DeploymentAgnosticFtrProv 'securitySolutionCasesV3', 'securitySolutionNotes', 'securitySolutionTimeline', + 'securitySolutionSiemMigrations', 'siem', 'siemV2', 'slo', 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' ) ); From 34f738f4b86cdea4ba66a9a59356b2e666b9cc7f Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Fri, 31 Jan 2025 17:18:40 +0100 Subject: [PATCH 20/25] fix type --- .../packages/side_nav/src/solution_side_nav_panel.test.tsx | 2 -- 1 file changed, 2 deletions(-) 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 f3c17f2c0b629..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 @@ -47,8 +47,6 @@ const mockItems: SolutionSideNavItem[] = [ }, ]; -const betaMockItemsCount = mockItems.filter((item) => item.isBeta).length; - const mockCategories: LinkCategories = [ { label: 'HOSTS CATEGORY', From 0275b89c367c03d6193f3f93b303fb12b5e137e0 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Mon, 3 Feb 2025 17:47:47 +0100 Subject: [PATCH 21/25] fix snapshot --- .../spaces_api_integration/common/suites/create.agnostic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d0ae38050a103..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,8 +97,8 @@ export function createTestSuiteFactory({ getService }: DeploymentAgnosticFtrProv 'securitySolutionCasesV2', 'securitySolutionCasesV3', 'securitySolutionNotes', - 'securitySolutionTimeline', 'securitySolutionSiemMigrations', + 'securitySolutionTimeline', 'siem', 'siemV2', 'slo', From 93920eeef8b61c6cc59e8a57e2cf3eede08145a4 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Tue, 4 Feb 2025 11:19:02 +0100 Subject: [PATCH 22/25] yet another snapshot update --- x-pack/test/spaces_api_integration/common/suites/get.agnostic.ts | 1 + .../spaces_api_integration/common/suites/get_all.agnostic.ts | 1 + 2 files changed, 2 insertions(+) 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', From 7a40256f00afde4a298bb84aabeeaeeb73ef83d9 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Wed, 5 Feb 2025 15:40:12 +0100 Subject: [PATCH 23/25] privileges callout styles improved --- .../missing_privileges/missing_privileges.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) 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 index 4a5f515ea2199..65ee7124e972f 100644 --- 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 @@ -6,7 +6,15 @@ */ import type { PropsWithChildren } from 'react'; import React from 'react'; -import { EuiCallOut, EuiCode, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +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[] }>( @@ -48,8 +56,13 @@ export const MissingPrivilegesTooltip = React.memo( 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} ); From e6bfaad04ef4b5645cc3c73d657fbd34ecd275c6 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 5 Feb 2025 16:10:41 +0000 Subject: [PATCH 24/25] [CI] Auto-commit changed files from 'yarn openapi:generate' --- .../common/api/quickstart_client.gen.ts | 4 +++- .../services/security_solution_api.gen.ts | 13 +++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) 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 80b057728b9ef..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 @@ -392,6 +392,7 @@ import type { StartRuleMigrationResponse, StopRuleMigrationRequestParamsInput, StopRuleMigrationResponse, + UpdateRuleMigrationRequestParamsInput, UpdateRuleMigrationRequestBodyInput, UpdateRuleMigrationResponse, UpsertRuleMigrationResourcesRequestParamsInput, @@ -2267,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', }, @@ -2616,6 +2617,7 @@ export interface UpdateRuleProps { body: UpdateRuleRequestBodyInput; } export interface UpdateRuleMigrationProps { + params: UpdateRuleMigrationRequestParamsInput; body: UpdateRuleMigrationRequestBodyInput; } export interface UpdateWorkflowInsightProps { 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 43e6f30b3376c..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, @@ -1587,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') @@ -1924,6 +1932,7 @@ export interface UpdateRuleProps { body: UpdateRuleRequestBodyInput; } export interface UpdateRuleMigrationProps { + params: UpdateRuleMigrationRequestParamsInput; body: UpdateRuleMigrationRequestBodyInput; } export interface UpdateWorkflowInsightProps { From 4cecde8c7ed8686a79fdecd9d5fd3bcf16e2ac39 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Thu, 6 Feb 2025 16:36:39 +0100 Subject: [PATCH 25/25] implement audit log functions --- .../lib/siem_migrations/rules/api/create.ts | 19 +-- .../lib/siem_migrations/rules/api/get.ts | 19 +-- .../lib/siem_migrations/rules/api/install.ts | 19 +-- .../rules/api/resources/get.ts | 19 +-- .../rules/api/resources/upsert.ts | 19 +-- .../lib/siem_migrations/rules/api/start.ts | 19 +-- .../lib/siem_migrations/rules/api/stop.ts | 19 +-- .../lib/siem_migrations/rules/api/update.ts | 23 ++- .../siem_migrations/rules/api/util/audit.ts | 159 +++++++++++++----- 9 files changed, 169 insertions(+), 146 deletions(-) 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 449f3ba98ed89..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,7 @@ 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'; @@ -54,10 +54,7 @@ export const registerSiemRuleMigrationsCreateRoute = ( const ctx = await context.resolve(['securitySolution']); const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); - await siemMigrationAuditLogger.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_CREATED, - id: migrationId, - }); + await siemMigrationAuditLogger.logCreateMigration({ migrationId }); const ruleMigrations = originalRules.map((originalRule) => ({ migration_id: migrationId, @@ -77,14 +74,10 @@ export const registerSiemRuleMigrationsCreateRoute = ( } return res.ok({ body: { migration_id: migrationId } }); - } catch (err) { - logger.error(err); - await siemMigrationAuditLogger.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_CREATED, - id: migrationId, - error: err, - }); - 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 6cb6e800dc248..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,7 @@ 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'; @@ -66,19 +66,12 @@ export const registerSiemRuleMigrationsGetRoute = ( const result = await ruleMigrationsClient.data.rules.get(migrationId, options); - await siemMigrationAuditLogger.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_RETRIEVED, - id: migrationId, - }); + await siemMigrationAuditLogger.logGetMigration({ migrationId }); return res.ok({ body: result }); - } catch (err) { - logger.error(err); - await siemMigrationAuditLogger.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_RETRIEVED, - id: migrationId, - error: err, - }); - 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/install.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/install.ts index 93fb43d4bd800..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,7 +14,7 @@ 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'; @@ -52,10 +52,7 @@ export const registerSiemRuleMigrationsInstallRoute = ( const savedObjectsClient = ctx.core.savedObjects.client; const rulesClient = await ctx.alerting.getRulesClient(); - await siemMigrationAuditLogger.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_INSTALLED_RULES, - id: migrationId, - }); + await siemMigrationAuditLogger.logInstallRules({ ids, migrationId }); const installed = await installTranslated({ migrationId, @@ -67,14 +64,10 @@ export const registerSiemRuleMigrationsInstallRoute = ( }); return res.ok({ body: { installed } }); - } catch (err) { - logger.error(err); - await siemMigrationAuditLogger.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_INSTALLED_RULES, - id: migrationId, - error: err, - }); - 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/resources/get.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/get.ts index e9afb209d3f3c..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,7 @@ 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'; @@ -50,20 +50,13 @@ export const registerSiemRuleMigrationsResourceGetRoute = ( const options = { filters: { type, names }, from, size }; const resources = await ruleMigrationsClient.data.resources.get(migrationId, options); - await siemMigrationAuditLogger.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_RETRIEVED_RESOURCES, - id: migrationId, - }); + await siemMigrationAuditLogger.logGetResources({ migrationId }); return res.ok({ body: resources }); - } catch (err) { - logger.error(err); - await siemMigrationAuditLogger.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_RETRIEVED_RESOURCES, - id: migrationId, - error: err, - }); - 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/upsert.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/resources/upsert.ts index 6b666d4688ec1..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,7 @@ 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'; @@ -56,10 +56,7 @@ export const registerSiemRuleMigrationsResourceUpsertRoute = ( const ctx = await context.resolve(['securitySolution']); const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); - await siemMigrationAuditLogger.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_UPLOADED_RESOURCES, - id: migrationId, - }); + await siemMigrationAuditLogger.logUploadResources({ migrationId }); // Check if the migration exists const { data } = await ruleMigrationsClient.data.rules.get(migrationId, { size: 1 }); @@ -89,14 +86,10 @@ export const registerSiemRuleMigrationsResourceUpsertRoute = ( await ruleMigrationsClient.data.resources.create(resourcesToCreate); return res.ok({ body: { acknowledged: true } }); - } catch (err) { - logger.error(err); - await siemMigrationAuditLogger.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_UPLOADED_RESOURCES, - 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 1a72565fd3573..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,7 @@ 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'; @@ -85,20 +85,13 @@ export const registerSiemRuleMigrationsStartRoute = ( return res.noContent(); } - await siemMigrationAuditLogger.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_STARTED, - id: migrationId, - }); + await siemMigrationAuditLogger.logStart({ migrationId }); return res.ok({ body: { started } }); - } catch (err) { - logger.error(err); - await siemMigrationAuditLogger.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_STARTED, - id: migrationId, - error: err, - }); - 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/stop.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/stop.ts index 325b12545665b..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,7 @@ 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'; @@ -47,20 +47,13 @@ export const registerSiemRuleMigrationsStopRoute = ( if (!exists) { return res.noContent(); } - await siemMigrationAuditLogger.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_STOPPED, - id: migrationId, - }); + await siemMigrationAuditLogger.logStop({ migrationId }); return res.ok({ body: { stopped } }); - } catch (err) { - logger.error(err); - await siemMigrationAuditLogger.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_STOPPED, - id: migrationId, - error: err, - }); - 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/update.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/update.ts index 605c6fe2179aa..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 @@ -15,7 +15,7 @@ import { } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { authz } from './util/authz'; -import { SiemMigrationAuditLogger, SiemMigrationsAuditActions } from './util/audit'; +import { SiemMigrationAuditLogger } from './util/audit'; import { transformToInternalUpdateRuleMigrationData } from './util/update_rules'; import { withLicense } from './util/with_license'; @@ -44,28 +44,25 @@ export const registerSiemRuleMigrationsUpdateRoute = ( const { migration_id: migrationId } = req.params; const rulesToUpdate = req.body; + 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(); - await siemMigrationAuditLogger.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_UPDATED_RULES, - id: migrationId, - }); + + 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); - await siemMigrationAuditLogger.log({ - action: SiemMigrationsAuditActions.SIEM_MIGRATION_UPDATED_RULES, - id: migrationId, - error: err, - }); - 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 65862d1a961aa..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 @@ -10,15 +10,14 @@ 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_RULES = 'siem_migration_installed_rules', - SIEM_MIGRATION_UPDATED_RULES = 'siem_migration_updated_rules', 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_INSTALLED_RULES = 'siem_migration_installed_rules', } export enum AUDIT_TYPE { @@ -46,33 +45,20 @@ 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_rules: AUDIT_TYPE.CREATION, - siem_migration_updated_rules: AUDIT_TYPE.CHANGE, - siem_migration_uploaded_resources: AUDIT_TYPE.CREATION, - siem_migration_retrieved_resources: AUDIT_TYPE.ACCESS, + [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 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_rules: 'User installed detection rules through SIEM migration', - siem_migration_updated_rules: 'User updated translated detection rules', - siem_migration_uploaded_resources: 'User uploaded resources through SIEM migration', - siem_migration_retrieved_resources: 'User retrieved SIEM migration resources', -}; - -export interface SiemMigrationAuditEvent { +interface SiemMigrationAuditEvent { action: SiemMigrationsAuditActions; + message: string; error?: Error; - id?: string; } export class SiemMigrationAuditLogger { @@ -81,27 +67,17 @@ export class SiemMigrationAuditLogger { private readonly securitySolutionContextPromise: Promise ) {} - private getAuditLogger = async (): Promise => { + private setAuditLogger = async (): Promise => { if (this.auditLogger === null) { const securitySolutionContext = await this.securitySolutionContextPromise; this.auditLogger = securitySolutionContext.getAuditLogger(); } - return this.auditLogger; + return !!this.auditLogger; }; - public async log({ action, error, id }: SiemMigrationAuditEvent): Promise { - const auditLogger = await this.getAuditLogger(); - if (!auditLogger) { - return; - } + private logEvent = ({ action, message, error }: SiemMigrationAuditEvent): void => { const type = siemMigrationAuditEventType[action]; - let message = siemMigrationAuditEventMessage[action]; - - if (id) { - message += ` with [id=${id}]`; - } - - auditLogger.log({ + this.auditLogger?.log({ message, event: { action, @@ -114,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); } }