diff --git a/src/platform/packages/shared/kbn-utility-types/index.ts b/src/platform/packages/shared/kbn-utility-types/index.ts index 78626bc7fdc55..83c0b91b9661f 100644 --- a/src/platform/packages/shared/kbn-utility-types/index.ts +++ b/src/platform/packages/shared/kbn-utility-types/index.ts @@ -111,6 +111,12 @@ export type PublicMethodsOf = Pick>; export type Writable = { -readonly [K in keyof T]: T[K]; }; +/** + * Makes an object with readonly properties mutable. + */ +export type RecursiveWritable = { + -readonly [K in keyof T]: RecursiveWritable; +}; /** * XOR for some properties applied to a type diff --git a/x-pack/solutions/security/packages/features/config.ts b/x-pack/solutions/security/packages/features/config.ts deleted file mode 100644 index 76939ed531e63..0000000000000 --- a/x-pack/solutions/security/packages/features/config.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { securityDefaultProductFeaturesConfig } from './src/security/product_feature_config'; -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 { timelineDefaultProductFeaturesConfig } from './src/timeline/product_feature_config'; -export { notesDefaultProductFeaturesConfig } from './src/notes/product_feature_config'; -export { siemMigrationsDefaultProductFeaturesConfig } from './src/siem_migrations/product_feature_config'; - -export { createEnabledProductFeaturesConfigMap } from './src/helpers'; diff --git a/x-pack/solutions/security/packages/features/src/assistant/index.ts b/x-pack/solutions/security/packages/features/src/assistant/index.ts index ea0658d795455..ad83b98ebbc2a 100644 --- a/x-pack/solutions/security/packages/features/src/assistant/index.ts +++ b/x-pack/solutions/security/packages/features/src/assistant/index.ts @@ -4,18 +4,20 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { AssistantSubFeatureId } from '../product_features_keys'; +import type { AssistantSubFeatureId, ProductFeatureAssistantKey } from '../product_features_keys'; import type { ProductFeatureParams } from '../types'; import { getAssistantBaseKibanaFeature } from './kibana_features'; import { getAssistantBaseKibanaSubFeatureIds, getAssistantSubFeaturesMap, } from './kibana_sub_features'; +import { assistantProductFeaturesConfig } from './product_feature_config'; export const getAssistantFeature = ( experimentalFeatures: Record -): ProductFeatureParams => ({ +): ProductFeatureParams => ({ baseKibanaFeature: getAssistantBaseKibanaFeature(), baseKibanaSubFeatureIds: getAssistantBaseKibanaSubFeatureIds(), subFeaturesMap: getAssistantSubFeaturesMap(experimentalFeatures), + productFeatureConfig: assistantProductFeaturesConfig, }); diff --git a/x-pack/solutions/security/packages/features/src/assistant/product_feature_config.ts b/x-pack/solutions/security/packages/features/src/assistant/product_feature_config.ts index 67c352afcfed7..f0ddf054e3f79 100644 --- a/x-pack/solutions/security/packages/features/src/assistant/product_feature_config.ts +++ b/x-pack/solutions/security/packages/features/src/assistant/product_feature_config.ts @@ -6,21 +6,11 @@ */ import { AssistantSubFeatureId, ProductFeatureAssistantKey } from '../product_features_keys'; -import type { ProductFeatureKibanaConfig } from '../types'; +import type { ProductFeaturesConfig } from '../types'; -/** - * App features privileges configuration for the Security Assistant Kibana Feature app. - * 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 assistantDefaultProductFeaturesConfig: Record< +export const assistantProductFeaturesConfig: ProductFeaturesConfig< ProductFeatureAssistantKey, - ProductFeatureKibanaConfig + AssistantSubFeatureId > = { [ProductFeatureAssistantKey.assistant]: { privileges: { diff --git a/x-pack/solutions/security/packages/features/src/attack_discovery/index.ts b/x-pack/solutions/security/packages/features/src/attack_discovery/index.ts index c72485c8f229e..7289f6fe0b150 100644 --- a/x-pack/solutions/security/packages/features/src/attack_discovery/index.ts +++ b/x-pack/solutions/security/packages/features/src/attack_discovery/index.ts @@ -7,9 +7,11 @@ import { getAttackDiscoveryBaseKibanaFeature } from './kibana_features'; import type { ProductFeatureParams } from '../types'; +import { attackDiscoveryProductFeaturesConfig } from './product_feature_config'; +import type { ProductFeatureAttackDiscoveryKey } from '../product_features_keys'; -export const getAttackDiscoveryFeature = (): ProductFeatureParams => ({ - baseKibanaFeature: getAttackDiscoveryBaseKibanaFeature(), - baseKibanaSubFeatureIds: [], - subFeaturesMap: new Map(), -}); +export const getAttackDiscoveryFeature = + (): ProductFeatureParams => ({ + baseKibanaFeature: getAttackDiscoveryBaseKibanaFeature(), + productFeatureConfig: attackDiscoveryProductFeaturesConfig, + }); diff --git a/x-pack/solutions/security/packages/features/src/attack_discovery/product_feature_config.ts b/x-pack/solutions/security/packages/features/src/attack_discovery/product_feature_config.ts index 94af601307849..fd0e6b104cb96 100644 --- a/x-pack/solutions/security/packages/features/src/attack_discovery/product_feature_config.ts +++ b/x-pack/solutions/security/packages/features/src/attack_discovery/product_feature_config.ts @@ -6,28 +6,16 @@ */ import { ProductFeatureAttackDiscoveryKey } from '../product_features_keys'; -import type { ProductFeatureKibanaConfig } from '../types'; +import type { ProductFeaturesConfig } 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 attackDiscoveryDefaultProductFeaturesConfig: Record< - ProductFeatureAttackDiscoveryKey, - ProductFeatureKibanaConfig -> = { - [ProductFeatureAttackDiscoveryKey.attackDiscovery]: { - privileges: { - all: { - ui: ['attack-discovery'], +export const attackDiscoveryProductFeaturesConfig: ProductFeaturesConfig = + { + [ProductFeatureAttackDiscoveryKey.attackDiscovery]: { + privileges: { + all: { + ui: ['attack-discovery'], + }, }, + subFeatureIds: [], }, - subFeatureIds: [], - }, -}; + }; diff --git a/x-pack/solutions/security/packages/features/src/cases/index.ts b/x-pack/solutions/security/packages/features/src/cases/index.ts index 8cab878d0dff8..2b60a7bb9a5ae 100644 --- a/x-pack/solutions/security/packages/features/src/cases/index.ts +++ b/x-pack/solutions/security/packages/features/src/cases/index.ts @@ -4,12 +4,12 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { CasesSubFeatureId } from '../product_features_keys'; +import type { CasesSubFeatureId, ProductFeatureCasesKey } from '../product_features_keys'; import type { ProductFeatureParams } from '../types'; import { getCasesBaseKibanaFeature } from './v1_features/kibana_features'; import { - getCasesBaseKibanaSubFeatureIds, - getCasesSubFeaturesMap, + getCasesBaseKibanaSubFeatureIdsV1, + getCasesSubFeaturesMapV1, } from './v1_features/kibana_sub_features'; import type { CasesFeatureParams } from './types'; import { getCasesBaseKibanaFeatureV2 } from './v2_features/kibana_features'; @@ -22,30 +22,31 @@ import { getCasesBaseKibanaSubFeatureIdsV3, getCasesSubFeaturesMapV3, } from './v3_features/kibana_sub_features'; +import { getCasesProductFeaturesConfig } from './product_feature_config'; -/** - * @deprecated Use getCasesV2Feature instead - */ export const getCasesFeature = ( params: CasesFeatureParams -): ProductFeatureParams => ({ +): ProductFeatureParams => ({ baseKibanaFeature: getCasesBaseKibanaFeature(params), - baseKibanaSubFeatureIds: getCasesBaseKibanaSubFeatureIds(), - subFeaturesMap: getCasesSubFeaturesMap(params), + baseKibanaSubFeatureIds: getCasesBaseKibanaSubFeatureIdsV1(), + subFeaturesMap: getCasesSubFeaturesMapV1(params), + productFeatureConfig: getCasesProductFeaturesConfig(params), }); export const getCasesV2Feature = ( params: CasesFeatureParams -): ProductFeatureParams => ({ +): ProductFeatureParams => ({ baseKibanaFeature: getCasesBaseKibanaFeatureV2(params), baseKibanaSubFeatureIds: getCasesBaseKibanaSubFeatureIdsV2(), subFeaturesMap: getCasesSubFeaturesMapV2(params), + productFeatureConfig: getCasesProductFeaturesConfig(params), }); export const getCasesV3Feature = ( params: CasesFeatureParams -): ProductFeatureParams => ({ +): ProductFeatureParams => ({ baseKibanaFeature: getCasesBaseKibanaFeatureV3(params), baseKibanaSubFeatureIds: getCasesBaseKibanaSubFeatureIdsV3(), subFeaturesMap: getCasesSubFeaturesMapV3(params), + productFeatureConfig: getCasesProductFeaturesConfig(params), }); diff --git a/x-pack/solutions/security/packages/features/src/cases/kibana_sub_features.ts b/x-pack/solutions/security/packages/features/src/cases/kibana_sub_features.ts new file mode 100644 index 0000000000000..30d13a6dfb1cf --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/cases/kibana_sub_features.ts @@ -0,0 +1,175 @@ +/* + * 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'; +import type { SubFeatureConfig } from '@kbn/features-plugin/common'; +import { APP_ID } from '../constants'; +import type { CasesFeatureParams } from './types'; + +export const getDeleteCasesSubFeature = ({ + apiTags, + uiCapabilities, + savedObjects, +}: CasesFeatureParams): SubFeatureConfig => ({ + name: i18n.translate('securitySolutionPackages.features.featureRegistry.deleteSubFeatureName', { + defaultMessage: 'Delete', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + api: apiTags.default.delete, + id: 'cases_delete', + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.deleteSubFeatureDetails', + { defaultMessage: 'Delete cases and comments' } + ), + includeIn: 'all', + savedObject: { + all: [...savedObjects.files], + read: [...savedObjects.files], + }, + cases: { + delete: [APP_ID], + }, + ui: uiCapabilities.default.delete, + }, + ], + }, + ], +}); + +export const getCasesSettingsCasesSubFeature = ({ + uiCapabilities, + savedObjects, +}: CasesFeatureParams): SubFeatureConfig => ({ + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.casesSettingsSubFeatureName', + { defaultMessage: 'Case settings' } + ), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'cases_settings', + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.casesSettingsSubFeatureDetails', + { defaultMessage: 'Edit case settings' } + ), + includeIn: 'all', + savedObject: { + all: [...savedObjects.files], + read: [...savedObjects.files], + }, + cases: { + settings: [APP_ID], + }, + ui: uiCapabilities.default.settings, + }, + ], + }, + ], +}); + +export const getCasesAddCommentsCasesSubFeature = ({ + apiTags, + uiCapabilities, + savedObjects, +}: CasesFeatureParams): SubFeatureConfig => ({ + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.addCommentsSubFeatureName', + { defaultMessage: 'Create comments & attachments' } + ), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + api: apiTags.default.createComment, + id: 'create_comment', + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.addCommentsSubFeatureDetails', + { defaultMessage: 'Add comments to cases' } + ), + includeIn: 'all', + savedObject: { + all: [...savedObjects.files], + read: [...savedObjects.files], + }, + cases: { + createComment: [APP_ID], + }, + ui: uiCapabilities.default.createComment, + }, + ], + }, + ], +}); + +export const getCasesReopenCaseSubFeature = ({ + uiCapabilities, +}: CasesFeatureParams): SubFeatureConfig => ({ + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.reopenCaseSubFeatureName', + { defaultMessage: 'Re-open' } + ), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'case_reopen', + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.reopenCaseSubFeatureDetails', + { defaultMessage: 'Re-open closed cases' } + ), + includeIn: 'all', + savedObject: { + all: [], + read: [], + }, + cases: { + reopenCase: [APP_ID], + }, + ui: uiCapabilities.default.reopenCase, + }, + ], + }, + ], +}); + +export const getCasesAssignUsersCasesSubFeature = ({ + uiCapabilities, +}: CasesFeatureParams): SubFeatureConfig => ({ + name: i18n.translate('securitySolutionPackages.features.assignUsersSubFeatureName', { + defaultMessage: 'Assign users', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'cases_assign', + name: i18n.translate('securitySolutionPackages.features.assignUsersSubFeatureName', { + defaultMessage: 'Assign users to cases', + }), + includeIn: 'all', + savedObject: { + all: [], + read: [], + }, + cases: { + assign: [APP_ID], + }, + ui: uiCapabilities.default.assignCase, + }, + ], + }, + ], +}); diff --git a/x-pack/solutions/security/packages/features/src/cases/product_feature_config.ts b/x-pack/solutions/security/packages/features/src/cases/product_feature_config.ts index 88cd22c795ecc..6c5d456eb4dd3 100644 --- a/x-pack/solutions/security/packages/features/src/cases/product_feature_config.ts +++ b/x-pack/solutions/security/packages/features/src/cases/product_feature_config.ts @@ -7,37 +7,24 @@ import { ProductFeatureCasesKey } from '../product_features_keys'; import { APP_ID } from '../constants'; -import type { DefaultCasesProductFeaturesConfig } from './types'; +import type { CasesFeatureParams, CasesProductFeaturesConfig } from './types'; -/** - * App features privileges configuration for the Security Cases Kibana Feature app. - * 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 getCasesDefaultProductFeaturesConfig = ({ +export const getCasesProductFeaturesConfig = ({ apiTags, uiCapabilities, -}: { - apiTags: { connectors: string }; - uiCapabilities: { connectors: string }; -}): DefaultCasesProductFeaturesConfig => ({ +}: CasesFeatureParams): CasesProductFeaturesConfig => ({ [ProductFeatureCasesKey.casesConnectors]: { privileges: { all: { - api: [apiTags.connectors], - ui: [uiCapabilities.connectors], + api: apiTags.connectors.all, + ui: uiCapabilities.connectors.all, cases: { push: [APP_ID], }, }, read: { - api: [apiTags.connectors], - ui: [uiCapabilities.connectors], + api: apiTags.connectors.read, + ui: uiCapabilities.connectors.read, }, }, }, diff --git a/x-pack/solutions/security/packages/features/src/cases/types.ts b/x-pack/solutions/security/packages/features/src/cases/types.ts index 17fb10fdd64ee..05ac8a182841b 100644 --- a/x-pack/solutions/security/packages/features/src/cases/types.ts +++ b/x-pack/solutions/security/packages/features/src/cases/types.ts @@ -6,15 +6,21 @@ */ import type { CasesUiCapabilities, CasesApiTags } from '@kbn/cases-plugin/common'; import type { ProductFeatureCasesKey, CasesSubFeatureId } from '../product_features_keys'; -import type { ProductFeatureKibanaConfig } from '../types'; +import type { ProductFeaturesConfig } from '../types'; export interface CasesFeatureParams { - uiCapabilities: CasesUiCapabilities; - apiTags: CasesApiTags; + apiTags: { + default: CasesApiTags; + connectors: Pick; + }; + uiCapabilities: { + default: CasesUiCapabilities; + connectors: Pick; + }; savedObjects: { files: string[] }; } -export type DefaultCasesProductFeaturesConfig = Record< +export type CasesProductFeaturesConfig = ProductFeaturesConfig< ProductFeatureCasesKey, - ProductFeatureKibanaConfig + CasesSubFeatureId >; 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 0aa47c2694670..d62fcb5bc5440 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 @@ -13,12 +13,9 @@ import type { BaseKibanaFeatureConfig } from '../../types'; import { APP_ID, CASES_FEATURE_ID, CASES_FEATURE_ID_V3 } from '../../constants'; import type { CasesFeatureParams } from '../types'; -/** - * @deprecated Use getCasesBaseKibanaFeatureV2 instead - */ export const getCasesBaseKibanaFeature = ({ - uiCapabilities, apiTags, + uiCapabilities, savedObjects, }: CasesFeatureParams): BaseKibanaFeatureConfig => { return { @@ -50,7 +47,7 @@ export const getCasesBaseKibanaFeature = ({ cases: [APP_ID], privileges: { all: { - api: [...apiTags.all, ...apiTags.createComment], + api: [...apiTags.default.all, ...apiTags.default.createComment], app: [CASES_FEATURE_ID, 'kibana'], catalogue: [APP_ID], cases: { @@ -67,10 +64,10 @@ export const getCasesBaseKibanaFeature = ({ read: [...savedObjects.files], }, ui: [ - ...uiCapabilities.all, - ...uiCapabilities.createComment, - ...uiCapabilities.reopenCase, - ...uiCapabilities.assignCase, + ...uiCapabilities.default.all, + ...uiCapabilities.default.createComment, + ...uiCapabilities.default.reopenCase, + ...uiCapabilities.default.assignCase, ], replacedBy: { default: [{ feature: CASES_FEATURE_ID_V3, privileges: ['all'] }], @@ -83,7 +80,7 @@ export const getCasesBaseKibanaFeature = ({ }, }, read: { - api: apiTags.read, + api: apiTags.default.read, app: [CASES_FEATURE_ID, 'kibana'], catalogue: [APP_ID], cases: { @@ -93,7 +90,7 @@ export const getCasesBaseKibanaFeature = ({ all: [], read: [...savedObjects.files], }, - ui: uiCapabilities.read, + ui: uiCapabilities.default.read, replacedBy: { default: [{ feature: CASES_FEATURE_ID_V3, privileges: ['read'] }], minimal: [{ feature: CASES_FEATURE_ID_V3, privileges: ['minimal_read'] }], diff --git a/x-pack/solutions/security/packages/features/src/cases/v1_features/kibana_sub_features.ts b/x-pack/solutions/security/packages/features/src/cases/v1_features/kibana_sub_features.ts index 07c33a31800e1..2378027af62b1 100644 --- a/x-pack/solutions/security/packages/features/src/cases/v1_features/kibana_sub_features.ts +++ b/x-pack/solutions/security/packages/features/src/cases/v1_features/kibana_sub_features.ts @@ -5,101 +5,31 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; import type { SubFeatureConfig } from '@kbn/features-plugin/common'; import { CasesSubFeatureId } from '../../product_features_keys'; -import { APP_ID, CASES_FEATURE_ID_V3 } from '../../constants'; +import { CASES_FEATURE_ID_V3 } from '../../constants'; import type { CasesFeatureParams } from '../types'; +import { addAllSubFeatureReplacements } from '../../utils'; +import { getDeleteCasesSubFeature, getCasesSettingsCasesSubFeature } from '../kibana_sub_features'; /** * Sub-features that will always be available for Security Cases * regardless of the product type. */ -export const getCasesBaseKibanaSubFeatureIds = (): CasesSubFeatureId[] => [ +export const getCasesBaseKibanaSubFeatureIdsV1 = (): CasesSubFeatureId[] => [ CasesSubFeatureId.deleteCases, CasesSubFeatureId.casesSettings, ]; /** - * @deprecated Use getCasesSubFeaturesMapV2 instead - * @description - Defines all the Security Solution Cases available. + * Defines all the Security Solution Cases subFeatures available. * The order of the subFeatures is the order they will be displayed */ -export const getCasesSubFeaturesMap = ({ - uiCapabilities, - apiTags, - savedObjects, -}: CasesFeatureParams) => { - const deleteCasesSubFeature: SubFeatureConfig = { - name: i18n.translate('securitySolutionPackages.features.featureRegistry.deleteSubFeatureName', { - defaultMessage: 'Delete', - }), - privilegeGroups: [ - { - groupType: 'independent', - privileges: [ - { - api: apiTags.delete, - id: 'cases_delete', - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.deleteSubFeatureDetails', - { - defaultMessage: 'Delete cases and comments', - } - ), - includeIn: 'all', - savedObject: { - all: [...savedObjects.files], - read: [...savedObjects.files], - }, - cases: { - delete: [APP_ID], - }, - ui: uiCapabilities.delete, - replacedBy: [{ feature: CASES_FEATURE_ID_V3, privileges: ['cases_delete'] }], - }, - ], - }, - ], - }; - - const casesSettingsCasesSubFeature: SubFeatureConfig = { - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.casesSettingsSubFeatureName', - { - defaultMessage: 'Case settings', - } - ), - privilegeGroups: [ - { - groupType: 'independent', - privileges: [ - { - id: 'cases_settings', - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.casesSettingsSubFeatureDetails', - { - defaultMessage: 'Edit case settings', - } - ), - includeIn: 'all', - savedObject: { - all: [...savedObjects.files], - read: [...savedObjects.files], - }, - cases: { - settings: [APP_ID], - }, - ui: uiCapabilities.settings, - replacedBy: [{ feature: CASES_FEATURE_ID_V3, privileges: ['cases_settings'] }], - }, - ], - }, - ], - }; - - return new Map([ - [CasesSubFeatureId.deleteCases, deleteCasesSubFeature], - [CasesSubFeatureId.casesSettings, casesSettingsCasesSubFeature], +export const getCasesSubFeaturesMapV1 = (params: CasesFeatureParams) => { + const subFeaturesMap = new Map([ + [CasesSubFeatureId.deleteCases, getDeleteCasesSubFeature(params)], + [CasesSubFeatureId.casesSettings, getCasesSettingsCasesSubFeature(params)], ]); + + return addAllSubFeatureReplacements(subFeaturesMap, [{ feature: CASES_FEATURE_ID_V3 }]); }; diff --git a/x-pack/solutions/security/packages/features/src/cases/v1_features/types.ts b/x-pack/solutions/security/packages/features/src/cases/v1_features/types.ts deleted file mode 100644 index f17f83ddecce8..0000000000000 --- a/x-pack/solutions/security/packages/features/src/cases/v1_features/types.ts +++ /dev/null @@ -1,14 +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 { ProductFeatureCasesKey, CasesSubFeatureId } from '../../product_features_keys'; -import type { ProductFeatureKibanaConfig } from '../../types'; - -export type DefaultCasesProductFeaturesConfig = Record< - ProductFeatureCasesKey, - ProductFeatureKibanaConfig ->; 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 2eb92cfd7ab4a..b28644caa2fe6 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 @@ -19,8 +19,8 @@ import { import type { CasesFeatureParams } from '../types'; export const getCasesBaseKibanaFeatureV2 = ({ - uiCapabilities, apiTags, + uiCapabilities, savedObjects, }: CasesFeatureParams): BaseKibanaFeatureConfig => { return { @@ -52,7 +52,7 @@ export const getCasesBaseKibanaFeatureV2 = ({ cases: [APP_ID], privileges: { all: { - api: apiTags.all, + api: apiTags.default.all, app: [CASES_FEATURE_ID, 'kibana'], catalogue: [APP_ID], cases: { @@ -66,7 +66,7 @@ export const getCasesBaseKibanaFeatureV2 = ({ all: [...savedObjects.files], read: [...savedObjects.files], }, - ui: [...uiCapabilities.all, ...uiCapabilities.assignCase], + ui: [...uiCapabilities.default.all, ...uiCapabilities.default.assignCase], replacedBy: { default: [{ feature: CASES_FEATURE_ID_V3, privileges: ['all'] }], minimal: [ @@ -78,7 +78,7 @@ export const getCasesBaseKibanaFeatureV2 = ({ }, }, read: { - api: apiTags.read, + api: apiTags.default.read, app: [CASES_FEATURE_ID, 'kibana'], catalogue: [APP_ID], cases: { @@ -88,7 +88,7 @@ export const getCasesBaseKibanaFeatureV2 = ({ all: [], read: [...savedObjects.files], }, - ui: uiCapabilities.read, + ui: uiCapabilities.default.read, replacedBy: { default: [{ feature: CASES_FEATURE_ID_V3, privileges: ['read'] }], minimal: [{ feature: CASES_FEATURE_ID_V3, privileges: ['minimal_read'] }], diff --git a/x-pack/solutions/security/packages/features/src/cases/v2_features/kibana_sub_features.ts b/x-pack/solutions/security/packages/features/src/cases/v2_features/kibana_sub_features.ts index ddc369ab45927..7b21768111905 100644 --- a/x-pack/solutions/security/packages/features/src/cases/v2_features/kibana_sub_features.ts +++ b/x-pack/solutions/security/packages/features/src/cases/v2_features/kibana_sub_features.ts @@ -5,11 +5,17 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; import type { SubFeatureConfig } from '@kbn/features-plugin/common'; import { CasesSubFeatureId } from '../../product_features_keys'; -import { APP_ID, CASES_FEATURE_ID_V3 } from '../../constants'; +import { CASES_FEATURE_ID_V3 } from '../../constants'; import type { CasesFeatureParams } from '../types'; +import { + getDeleteCasesSubFeature, + getCasesSettingsCasesSubFeature, + getCasesAddCommentsCasesSubFeature, + getCasesReopenCaseSubFeature, +} from '../kibana_sub_features'; +import { addAllSubFeatureReplacements } from '../../utils'; /** * Sub-features that will always be available for Security Cases @@ -26,156 +32,14 @@ export const getCasesBaseKibanaSubFeatureIdsV2 = (): CasesSubFeatureId[] => [ * Defines all the Security Solution Cases subFeatures available. * The order of the subFeatures is the order they will be displayed */ -export const getCasesSubFeaturesMapV2 = ({ - uiCapabilities, - apiTags, - savedObjects, -}: CasesFeatureParams) => { - const deleteCasesSubFeature: SubFeatureConfig = { - name: i18n.translate('securitySolutionPackages.features.featureRegistry.deleteSubFeatureName', { - defaultMessage: 'Delete', - }), - privilegeGroups: [ - { - groupType: 'independent', - privileges: [ - { - api: apiTags.delete, - id: 'cases_delete', - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.deleteSubFeatureDetails', - { - defaultMessage: 'Delete cases and comments', - } - ), - includeIn: 'all', - savedObject: { - all: [...savedObjects.files], - read: [...savedObjects.files], - }, - cases: { - delete: [APP_ID], - }, - ui: uiCapabilities.delete, - replacedBy: [{ feature: CASES_FEATURE_ID_V3, privileges: ['cases_delete'] }], - }, - ], - }, - ], - }; - - const casesSettingsCasesSubFeature: SubFeatureConfig = { - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.casesSettingsSubFeatureName', - { - defaultMessage: 'Case settings', - } - ), - privilegeGroups: [ - { - groupType: 'independent', - privileges: [ - { - id: 'cases_settings', - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.casesSettingsSubFeatureDetails', - { - defaultMessage: 'Edit case settings', - } - ), - includeIn: 'all', - savedObject: { - all: [...savedObjects.files], - read: [...savedObjects.files], - }, - cases: { - settings: [APP_ID], - }, - ui: uiCapabilities.settings, - replacedBy: [{ feature: CASES_FEATURE_ID_V3, privileges: ['cases_settings'] }], - }, - ], - }, - ], - }; - - /* The below sub features were newly added in v2 (8.17) */ - - const casesAddCommentsCasesSubFeature: SubFeatureConfig = { - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.addCommentsSubFeatureName', - { - defaultMessage: 'Create comments & attachments', - } - ), - privilegeGroups: [ - { - groupType: 'independent', - privileges: [ - { - api: apiTags.createComment, - id: 'create_comment', - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.addCommentsSubFeatureDetails', - { - defaultMessage: 'Add comments to cases', - } - ), - includeIn: 'all', - savedObject: { - all: [...savedObjects.files], - read: [...savedObjects.files], - }, - cases: { - createComment: [APP_ID], - }, - ui: uiCapabilities.createComment, - replacedBy: [{ feature: CASES_FEATURE_ID_V3, privileges: ['create_comment'] }], - }, - ], - }, - ], - }; - const casesreopenCaseSubFeature: SubFeatureConfig = { - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.reopenCaseSubFeatureName', - { - defaultMessage: 'Re-open', - } - ), - privilegeGroups: [ - { - groupType: 'independent', - privileges: [ - { - id: 'case_reopen', - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.reopenCaseSubFeatureDetails', - { - defaultMessage: 'Re-open closed cases', - } - ), - includeIn: 'all', - savedObject: { - all: [], - read: [], - }, - cases: { - reopenCase: [APP_ID], - }, - ui: uiCapabilities.reopenCase, - replacedBy: [{ feature: CASES_FEATURE_ID_V3, privileges: ['case_reopen'] }], - }, - ], - }, - ], - }; - - return new Map([ - [CasesSubFeatureId.deleteCases, deleteCasesSubFeature], - [CasesSubFeatureId.casesSettings, casesSettingsCasesSubFeature], +export const getCasesSubFeaturesMapV2 = (params: CasesFeatureParams) => { + const subFeaturesMap = new Map([ + [CasesSubFeatureId.deleteCases, getDeleteCasesSubFeature(params)], + [CasesSubFeatureId.casesSettings, getCasesSettingsCasesSubFeature(params)], /* The below sub features were newly added in v2 (8.17) */ - [CasesSubFeatureId.createComment, casesAddCommentsCasesSubFeature], - [CasesSubFeatureId.reopenCase, casesreopenCaseSubFeature], + [CasesSubFeatureId.createComment, getCasesAddCommentsCasesSubFeature(params)], + [CasesSubFeatureId.reopenCase, getCasesReopenCaseSubFeature(params)], ]); + + return addAllSubFeatureReplacements(subFeaturesMap, [{ feature: CASES_FEATURE_ID_V3 }]); }; diff --git a/x-pack/solutions/security/packages/features/src/cases/v3_features/kibana_features.ts b/x-pack/solutions/security/packages/features/src/cases/v3_features/kibana_features.ts index c9a08ebb8614d..58bd50b67cf14 100644 --- a/x-pack/solutions/security/packages/features/src/cases/v3_features/kibana_features.ts +++ b/x-pack/solutions/security/packages/features/src/cases/v3_features/kibana_features.ts @@ -14,8 +14,8 @@ import { APP_ID, CASES_FEATURE_ID_V3, CASES_FEATURE_ID } from '../../constants'; import type { CasesFeatureParams } from '../types'; export const getCasesBaseKibanaFeatureV3 = ({ - uiCapabilities, apiTags, + uiCapabilities, savedObjects, }: CasesFeatureParams): BaseKibanaFeatureConfig => { return { @@ -34,7 +34,7 @@ export const getCasesBaseKibanaFeatureV3 = ({ cases: [APP_ID], privileges: { all: { - api: apiTags.all, + api: apiTags.default.all, app: [CASES_FEATURE_ID, 'kibana'], catalogue: [APP_ID], cases: { @@ -47,10 +47,10 @@ export const getCasesBaseKibanaFeatureV3 = ({ all: [...savedObjects.files], read: [...savedObjects.files], }, - ui: uiCapabilities.all, + ui: uiCapabilities.default.all, }, read: { - api: apiTags.read, + api: apiTags.default.read, app: [CASES_FEATURE_ID, 'kibana'], catalogue: [APP_ID], cases: { @@ -60,7 +60,7 @@ export const getCasesBaseKibanaFeatureV3 = ({ all: [], read: [...savedObjects.files], }, - ui: uiCapabilities.read, + ui: uiCapabilities.default.read, }, }, }; diff --git a/x-pack/solutions/security/packages/features/src/cases/v3_features/kibana_sub_features.ts b/x-pack/solutions/security/packages/features/src/cases/v3_features/kibana_sub_features.ts index b1672d25d0c3b..0585eee3dd171 100644 --- a/x-pack/solutions/security/packages/features/src/cases/v3_features/kibana_sub_features.ts +++ b/x-pack/solutions/security/packages/features/src/cases/v3_features/kibana_sub_features.ts @@ -5,11 +5,16 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; import type { SubFeatureConfig } from '@kbn/features-plugin/common'; import { CasesSubFeatureId } from '../../product_features_keys'; -import { APP_ID } from '../../constants'; import type { CasesFeatureParams } from '../types'; +import { + getCasesAddCommentsCasesSubFeature, + getCasesAssignUsersCasesSubFeature, + getCasesReopenCaseSubFeature, + getCasesSettingsCasesSubFeature, + getDeleteCasesSubFeature, +} from '../kibana_sub_features'; /** * Sub-features that will always be available for Security Cases @@ -23,182 +28,13 @@ export const getCasesBaseKibanaSubFeatureIdsV3 = (): CasesSubFeatureId[] => [ CasesSubFeatureId.assignUsers, ]; -/** - * Defines all the Security Solution Cases subFeatures available. - * The order of the subFeatures is the order they will be displayed - */ -export const getCasesSubFeaturesMapV3 = ({ - uiCapabilities, - apiTags, - savedObjects, -}: CasesFeatureParams) => { - const deleteCasesSubFeature: SubFeatureConfig = { - name: i18n.translate('securitySolutionPackages.features.featureRegistry.deleteSubFeatureName', { - defaultMessage: 'Delete', - }), - privilegeGroups: [ - { - groupType: 'independent', - privileges: [ - { - api: apiTags.delete, - id: 'cases_delete', - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.deleteSubFeatureDetails', - { - defaultMessage: 'Delete cases and comments', - } - ), - includeIn: 'all', - savedObject: { - all: [...savedObjects.files], - read: [...savedObjects.files], - }, - cases: { - delete: [APP_ID], - }, - ui: uiCapabilities.delete, - }, - ], - }, - ], - }; - - const casesSettingsCasesSubFeature: SubFeatureConfig = { - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.casesSettingsSubFeatureName', - { - defaultMessage: 'Case settings', - } - ), - privilegeGroups: [ - { - groupType: 'independent', - privileges: [ - { - id: 'cases_settings', - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.casesSettingsSubFeatureDetails', - { - defaultMessage: 'Edit case settings', - } - ), - includeIn: 'all', - savedObject: { - all: [...savedObjects.files], - read: [...savedObjects.files], - }, - cases: { - settings: [APP_ID], - }, - ui: uiCapabilities.settings, - }, - ], - }, - ], - }; - - const casesAddCommentsCasesSubFeature: SubFeatureConfig = { - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.addCommentsSubFeatureName', - { - defaultMessage: 'Create comments & attachments', - } - ), - privilegeGroups: [ - { - groupType: 'independent', - privileges: [ - { - api: apiTags.createComment, - id: 'create_comment', - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.addCommentsSubFeatureDetails', - { - defaultMessage: 'Add comments to cases', - } - ), - includeIn: 'all', - savedObject: { - all: [...savedObjects.files], - read: [...savedObjects.files], - }, - cases: { - createComment: [APP_ID], - }, - ui: uiCapabilities.createComment, - }, - ], - }, - ], - }; - const casesreopenCaseSubFeature: SubFeatureConfig = { - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.reopenCaseSubFeatureName', - { - defaultMessage: 'Re-open', - } - ), - privilegeGroups: [ - { - groupType: 'independent', - privileges: [ - { - id: 'case_reopen', - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.reopenCaseSubFeatureDetails', - { - defaultMessage: 'Re-open closed cases', - } - ), - includeIn: 'all', - savedObject: { - all: [], - read: [], - }, - cases: { - reopenCase: [APP_ID], - }, - ui: uiCapabilities.reopenCase, - }, - ], - }, - ], - }; - - const casesAssignUsersCasesSubFeature: SubFeatureConfig = { - name: i18n.translate('securitySolutionPackages.features.assignUsersSubFeatureName', { - defaultMessage: 'Assign users', - }), - privilegeGroups: [ - { - groupType: 'independent', - privileges: [ - { - id: 'cases_assign', - name: i18n.translate('securitySolutionPackages.features.assignUsersSubFeatureName', { - defaultMessage: 'Assign users to cases', - }), - includeIn: 'all', - savedObject: { - all: [], - read: [], - }, - cases: { - assign: [APP_ID], - }, - ui: uiCapabilities.assignCase, - }, - ], - }, - ], - }; - +export const getCasesSubFeaturesMapV3 = (params: CasesFeatureParams) => { return new Map([ - [CasesSubFeatureId.deleteCases, deleteCasesSubFeature], - [CasesSubFeatureId.casesSettings, casesSettingsCasesSubFeature], - [CasesSubFeatureId.createComment, casesAddCommentsCasesSubFeature], - [CasesSubFeatureId.reopenCase, casesreopenCaseSubFeature], - [CasesSubFeatureId.assignUsers, casesAssignUsersCasesSubFeature], + [CasesSubFeatureId.deleteCases, getDeleteCasesSubFeature(params)], + [CasesSubFeatureId.casesSettings, getCasesSettingsCasesSubFeature(params)], + [CasesSubFeatureId.createComment, getCasesAddCommentsCasesSubFeature(params)], + [CasesSubFeatureId.reopenCase, getCasesReopenCaseSubFeature(params)], + /* The below sub features were newly added in V3 */ + [CasesSubFeatureId.assignUsers, getCasesAssignUsersCasesSubFeature(params)], ]); }; diff --git a/x-pack/solutions/security/packages/features/src/helpers.ts b/x-pack/solutions/security/packages/features/src/helpers.ts deleted file mode 100644 index 55baa89022c92..0000000000000 --- a/x-pack/solutions/security/packages/features/src/helpers.ts +++ /dev/null @@ -1,33 +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 { - ProductFeatureKeys, - ProductFeatureKeyType, - ProductFeatureKibanaConfig, -} from './types'; - -/** - * Creates the ProductFeaturesConfig Map from the given productFeatures object and a set of enabled productFeatures keys. - */ -export const createEnabledProductFeaturesConfigMap = < - K extends ProductFeatureKeyType, - T extends string = string ->( - productFeatures: Record>, - enabledProductFeaturesKeys: ProductFeatureKeys -) => - new Map( - Object.entries>(productFeatures).reduce< - Array<[K, ProductFeatureKibanaConfig]> - >((acc, [key, value]) => { - if (enabledProductFeaturesKeys.includes(key as K)) { - acc.push([key as K, value]); - } - return acc; - }, []) - ); diff --git a/x-pack/solutions/security/packages/features/src/notes/index.ts b/x-pack/solutions/security/packages/features/src/notes/index.ts index c6ec57b39bb8c..525b75cac58a8 100644 --- a/x-pack/solutions/security/packages/features/src/notes/index.ts +++ b/x-pack/solutions/security/packages/features/src/notes/index.ts @@ -8,9 +8,9 @@ import { getNotesBaseKibanaFeature } from './kibana_features'; import type { ProductFeatureParams } from '../types'; import type { SecurityFeatureParams } from '../security/types'; +import { notesProductFeaturesConfig } from './product_feature_config'; export const getNotesFeature = (params: SecurityFeatureParams): ProductFeatureParams => ({ baseKibanaFeature: getNotesBaseKibanaFeature(params), - baseKibanaSubFeatureIds: [], - subFeaturesMap: new Map(), + productFeatureConfig: notesProductFeaturesConfig, }); diff --git a/x-pack/solutions/security/packages/features/src/notes/product_feature_config.ts b/x-pack/solutions/security/packages/features/src/notes/product_feature_config.ts index 596b0af69b62e..ada9eaf65817b 100644 --- a/x-pack/solutions/security/packages/features/src/notes/product_feature_config.ts +++ b/x-pack/solutions/security/packages/features/src/notes/product_feature_config.ts @@ -5,24 +5,11 @@ * 2.0. */ -import { ProductFeatureNotesFeatureKey } from '../product_features_keys'; -import type { ProductFeatureKibanaConfig } from '../types'; +import { ProductFeatureNotesKey } from '../product_features_keys'; +import type { ProductFeaturesConfig } from '../types'; -/** - * App features privileges configuration for the notes 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 notesDefaultProductFeaturesConfig: Record< - ProductFeatureNotesFeatureKey, - ProductFeatureKibanaConfig -> = { - [ProductFeatureNotesFeatureKey.notes]: { +export const notesProductFeaturesConfig: ProductFeaturesConfig = { + [ProductFeatureNotesKey.notes]: { privileges: { all: { api: ['notes_read', 'notes_write'], 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 42626660c04d1..7bddff8c3e20b 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 @@ -137,14 +137,14 @@ export enum ProductFeatureAttackDiscoveryKey { attackDiscovery = 'attack_discovery', } -export enum ProductFeatureTimelineFeatureKey { +export enum ProductFeatureTimelineKey { /** * Enables Timeline */ timeline = 'timeline', } -export enum ProductFeatureNotesFeatureKey { +export enum ProductFeatureNotesKey { /** * Enables Notes */ @@ -164,8 +164,8 @@ export const ProductFeatureKey = { ...ProductFeatureAssistantKey, ...ProductFeatureAttackDiscoveryKey, ...ProductFeatureSiemMigrationsKey, - ...ProductFeatureTimelineFeatureKey, - ...ProductFeatureNotesFeatureKey, + ...ProductFeatureTimelineKey, + ...ProductFeatureNotesKey, }; // We need to merge the value and the type and export both to replicate how enum works. export type ProductFeatureKeyType = @@ -174,8 +174,8 @@ export type ProductFeatureKeyType = | ProductFeatureAssistantKey | ProductFeatureAttackDiscoveryKey | ProductFeatureSiemMigrationsKey - | ProductFeatureTimelineFeatureKey - | ProductFeatureNotesFeatureKey; + | ProductFeatureTimelineKey + | ProductFeatureNotesKey; export const ALL_PRODUCT_FEATURE_KEYS = Object.freeze(Object.values(ProductFeatureKey)); diff --git a/x-pack/solutions/security/packages/features/src/security/index.ts b/x-pack/solutions/security/packages/features/src/security/index.ts index ccad3cc1f9334..b0a01d9e9b5b5 100644 --- a/x-pack/solutions/security/packages/features/src/security/index.ts +++ b/x-pack/solutions/security/packages/features/src/security/index.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { SecuritySubFeatureId } from '../product_features_keys'; +import type { ProductFeatureSecurityKey, SecuritySubFeatureId } from '../product_features_keys'; import type { ProductFeatureParams } from '../types'; import { getSecurityBaseKibanaFeature } from './v1_features/kibana_features'; import { @@ -22,33 +22,33 @@ import { getSecurityV3BaseKibanaSubFeatureIds, getSecurityV3SubFeaturesMap, } from './v3_features/kibana_sub_features'; +import { securityDefaultProductFeaturesConfig } from './product_feature_config'; +import { securityV1ProductFeaturesConfig } from './v1_features/product_feature_config'; +import { securityV2ProductFeaturesConfig } from './v2_features/product_feature_config'; -/** - * @deprecated Use getSecurityV2Feature instead - */ export const getSecurityFeature = ( params: SecurityFeatureParams -): ProductFeatureParams => ({ +): ProductFeatureParams => ({ baseKibanaFeature: getSecurityBaseKibanaFeature(params), baseKibanaSubFeatureIds: getSecurityBaseKibanaSubFeatureIds(params), subFeaturesMap: getSecuritySubFeaturesMap(params), + productFeatureConfig: securityV1ProductFeaturesConfig, }); -/** - * @deprecated Use getSecurityV3Feature instead - */ export const getSecurityV2Feature = ( params: SecurityFeatureParams -): ProductFeatureParams => ({ +): ProductFeatureParams => ({ baseKibanaFeature: getSecurityV2BaseKibanaFeature(params), baseKibanaSubFeatureIds: getSecurityV2BaseKibanaSubFeatureIds(params), subFeaturesMap: getSecurityV2SubFeaturesMap(params), + productFeatureConfig: securityV2ProductFeaturesConfig, }); export const getSecurityV3Feature = ( params: SecurityFeatureParams -): ProductFeatureParams => ({ +): ProductFeatureParams => ({ baseKibanaFeature: getSecurityV3BaseKibanaFeature(params), baseKibanaSubFeatureIds: getSecurityV3BaseKibanaSubFeatureIds(params), subFeaturesMap: getSecurityV3SubFeaturesMap(params), + productFeatureConfig: securityDefaultProductFeaturesConfig, }); diff --git a/x-pack/solutions/security/packages/features/src/security/kibana_sub_features.ts b/x-pack/solutions/security/packages/features/src/security/kibana_sub_features.ts new file mode 100644 index 0000000000000..7627297beef89 --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/security/kibana_sub_features.ts @@ -0,0 +1,760 @@ +/* + * 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'; +import type { SubFeatureConfig } from '@kbn/features-plugin/common'; +import { EXCEPTION_LIST_NAMESPACE_AGNOSTIC } from '@kbn/securitysolution-list-constants'; + +import { APP_ID } from '../constants'; +import type { SecurityFeatureParams } from './types'; + +const TRANSLATIONS = Object.freeze({ + all: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.allPrivilegeName', + { defaultMessage: 'All' } + ), + read: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.readPrivilegeName', + { defaultMessage: 'Read' } + ), +}); + +export const endpointListSubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointList.privilegesTooltip', + { defaultMessage: 'All Spaces is required for Endpoint List access.' } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointList', + { defaultMessage: 'Endpoint List' } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointList.description', + { + defaultMessage: + 'Displays all hosts running Elastic Defend and their relevant integration details.', + } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + api: [`${APP_ID}-writeEndpointList`, `${APP_ID}-readEndpointList`], + id: 'endpoint_list_all', + includeIn: 'none', + name: TRANSLATIONS.all, + savedObject: { + all: [], + read: [], + }, + ui: ['writeEndpointList', 'readEndpointList'], + }, + { + api: [`${APP_ID}-readEndpointList`], + id: 'endpoint_list_read', + includeIn: 'none', + name: TRANSLATIONS.read, + savedObject: { + all: [], + read: [], + }, + ui: ['readEndpointList'], + }, + ], + }, + ], +}); + +export const trustedApplicationsSubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedApplications.privilegesTooltip', + { defaultMessage: 'All Spaces is required for Trusted Applications access.' } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedApplications', + { defaultMessage: 'Trusted Applications' } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedApplications.description', + { + defaultMessage: + 'Helps mitigate conflicts with other software, usually other antivirus or endpoint security applications.', + } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + api: [ + 'lists-all', + 'lists-read', + 'lists-summary', + `${APP_ID}-writeTrustedApplications`, + `${APP_ID}-readTrustedApplications`, + ], + id: 'trusted_applications_all', + includeIn: 'none', + name: TRANSLATIONS.all, + savedObject: { + all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC], + read: [], + }, + ui: ['writeTrustedApplications', 'readTrustedApplications'], + }, + { + api: ['lists-read', 'lists-summary', `${APP_ID}-readTrustedApplications`], + id: 'trusted_applications_read', + includeIn: 'none', + name: TRANSLATIONS.read, + savedObject: { + all: [], + read: [], + }, + ui: ['readTrustedApplications'], + }, + ], + }, + ], +}); + +export const trustedDevicesSubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedDevices.privilegesTooltip', + { + defaultMessage: 'All Spaces is required for Trusted Devices access.', + } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedDevices', + { + defaultMessage: 'Trusted Devices', + } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedDevices.description', + { + defaultMessage: + 'Allows management of trusted USB and external devices that bypass device control protections.', + } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + api: [ + 'lists-all', + 'lists-read', + 'lists-summary', + `${APP_ID}-writeTrustedDevices`, + `${APP_ID}-readTrustedDevices`, + ], + id: 'trusted_devices_all', + includeIn: 'none', + name: TRANSLATIONS.all, + savedObject: { + all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC], + read: [], + }, + ui: ['writeTrustedDevices', 'readTrustedDevices'], + }, + { + api: ['lists-read', 'lists-summary', `${APP_ID}-readTrustedDevices`], + id: 'trusted_devices_read', + includeIn: 'none', + name: TRANSLATIONS.read, + savedObject: { + all: [], + read: [], + }, + ui: ['readTrustedDevices'], + }, + ], + }, + ], +}); + +export const hostIsolationExceptionsBasicSubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolationExceptions.privilegesTooltip', + { defaultMessage: 'All Spaces is required for Host Isolation Exceptions access.' } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolationExceptions', + { defaultMessage: 'Host Isolation Exceptions' } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolationExceptions.description', + { + defaultMessage: + 'Add specific IP addresses that isolated hosts are still allowed to communicate with, even when isolated from the rest of the network.', + } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + api: [ + 'lists-all', + 'lists-read', + 'lists-summary', + `${APP_ID}-deleteHostIsolationExceptions`, + `${APP_ID}-readHostIsolationExceptions`, + ], + id: 'host_isolation_exceptions_all', + includeIn: 'none', + name: TRANSLATIONS.all, + savedObject: { + all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC], + read: [], + }, + ui: ['readHostIsolationExceptions', 'deleteHostIsolationExceptions'], + }, + { + api: ['lists-read', 'lists-summary', `${APP_ID}-readHostIsolationExceptions`], + id: 'host_isolation_exceptions_read', + includeIn: 'none', + name: TRANSLATIONS.read, + savedObject: { + all: [], + read: [], + }, + ui: ['readHostIsolationExceptions'], + }, + ], + }, + ], +}); +export const blocklistSubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.blockList.privilegesTooltip', + { defaultMessage: 'All Spaces is required for Blocklist access.' } + ), + name: i18n.translate('securitySolutionPackages.features.featureRegistry.subFeatures.blockList', { + defaultMessage: 'Blocklist', + }), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.blockList.description', + { + defaultMessage: + 'Extend Elastic Defend’s protection against malicious processes and protect against potentially harmful applications.', + } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + api: [ + 'lists-all', + 'lists-read', + 'lists-summary', + `${APP_ID}-writeBlocklist`, + `${APP_ID}-readBlocklist`, + ], + id: 'blocklist_all', + includeIn: 'none', + name: TRANSLATIONS.all, + savedObject: { + all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC], + read: [], + }, + ui: ['writeBlocklist', 'readBlocklist'], + }, + { + api: ['lists-read', 'lists-summary', `${APP_ID}-readBlocklist`], + id: 'blocklist_read', + includeIn: 'none', + name: TRANSLATIONS.read, + savedObject: { + all: [], + read: [], + }, + ui: ['readBlocklist'], + }, + ], + }, + ], +}); +export const eventFiltersSubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.eventFilters.privilegesTooltip', + { defaultMessage: 'All Spaces is required for Event Filters access.' } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.eventFilters', + { defaultMessage: 'Event Filters' } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.eventFilters.description', + { + defaultMessage: + 'Filter out endpoint events that you do not need or want stored in Elasticsearch.', + } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + api: [ + 'lists-all', + 'lists-read', + 'lists-summary', + `${APP_ID}-writeEventFilters`, + `${APP_ID}-readEventFilters`, + ], + id: 'event_filters_all', + includeIn: 'none', + name: TRANSLATIONS.all, + savedObject: { + all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC], + read: [], + }, + ui: ['writeEventFilters', 'readEventFilters'], + }, + { + api: ['lists-read', 'lists-summary', `${APP_ID}-readEventFilters`], + id: 'event_filters_read', + includeIn: 'none', + name: TRANSLATIONS.read, + savedObject: { + all: [], + read: [], + }, + ui: ['readEventFilters'], + }, + ], + }, + ], +}); +export const policyManagementSubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.policyManagement.privilegesTooltip', + { defaultMessage: 'All Spaces is required for Policy Management access.' } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.policyManagement', + { defaultMessage: 'Elastic Defend Policy Management' } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.policyManagement.description', + { + defaultMessage: + 'Access the Elastic Defend integration policy to configure protections, event collection, and advanced policy features.', + } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + api: [`${APP_ID}-writePolicyManagement`, `${APP_ID}-readPolicyManagement`], + id: 'policy_management_all', + includeIn: 'none', + name: TRANSLATIONS.all, + savedObject: { + all: ['policy-settings-protection-updates-note'], + read: [], + }, + ui: ['writePolicyManagement', 'readPolicyManagement'], + }, + { + api: [`${APP_ID}-readPolicyManagement`], + id: 'policy_management_read', + includeIn: 'none', + name: TRANSLATIONS.read, + savedObject: { + all: [], + read: ['policy-settings-protection-updates-note'], + }, + ui: ['readPolicyManagement'], + }, + ], + }, + ], +}); + +export const responseActionsHistorySubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.responseActionsHistory.privilegesTooltip', + { defaultMessage: 'All Spaces is required for Response Actions History access.' } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.responseActionsHistory', + { defaultMessage: 'Response Actions History' } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.responseActionsHistory.description', + { defaultMessage: 'Access the history of response actions performed on endpoints.' } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + api: [`${APP_ID}-writeActionsLogManagement`, `${APP_ID}-readActionsLogManagement`], + id: 'actions_log_management_all', + includeIn: 'none', + name: TRANSLATIONS.all, + savedObject: { + all: [], + read: [], + }, + ui: ['writeActionsLogManagement', 'readActionsLogManagement'], + }, + { + api: [`${APP_ID}-readActionsLogManagement`], + id: 'actions_log_management_read', + includeIn: 'none', + name: TRANSLATIONS.read, + savedObject: { + all: [], + read: [], + }, + ui: ['readActionsLogManagement'], + }, + ], + }, + ], +}); +export const hostIsolationSubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolation.privilegesTooltip', + { defaultMessage: 'All Spaces is required for Host Isolation access.' } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolation', + { defaultMessage: 'Host Isolation' } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolation.description', + { defaultMessage: 'Perform the "isolate" and "release" response actions.' } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + api: [`${APP_ID}-writeHostIsolationRelease`], + id: 'host_isolation_all', + includeIn: 'none', + name: TRANSLATIONS.all, + savedObject: { + all: [], + read: [], + }, + ui: ['writeHostIsolationRelease'], + }, + ], + }, + ], +}); + +export const processOperationsSubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.processOperations.privilegesTooltip', + { defaultMessage: 'All Spaces is required for Process Operations access.' } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.processOperations', + { defaultMessage: 'Process Operations' } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.processOperations.description', + { defaultMessage: 'Perform process-related response actions in the response console.' } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + api: [`${APP_ID}-writeProcessOperations`], + id: 'process_operations_all', + includeIn: 'none', + name: TRANSLATIONS.all, + savedObject: { + all: [], + read: [], + }, + ui: ['writeProcessOperations'], + }, + ], + }, + ], +}); +export const fileOperationsSubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.fileOperations.privilegesTooltip', + { defaultMessage: 'All Spaces is required for File Operations access.' } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.fileOperations', + { defaultMessage: 'File Operations' } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.fileOperations.description', + { defaultMessage: 'Perform file-related response actions in the response console.' } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + api: [`${APP_ID}-writeFileOperations`], + id: 'file_operations_all', + includeIn: 'none', + name: TRANSLATIONS.all, + savedObject: { + all: [], + read: [], + }, + ui: ['writeFileOperations'], + }, + ], + }, + ], +}); + +// execute operations are not available in 8.7, +// but will be available in 8.8 +export const executeActionSubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.executeOperations.privilegesTooltip', + { defaultMessage: 'All Spaces is required for Execute Operations access.' } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.executeOperations', + { defaultMessage: 'Execute Operations' } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.executeOperations.description', + { defaultMessage: 'Perform script execution response actions in the response console.' } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + api: [`${APP_ID}-writeExecuteOperations`], + id: 'execute_operations_all', + includeIn: 'none', + name: TRANSLATIONS.all, + savedObject: { + all: [], + read: [], + }, + ui: ['writeExecuteOperations'], + }, + ], + }, + ], +}); + +// 8.15 feature +export const scanActionSubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.scanOperations.privilegesTooltip', + { defaultMessage: 'All Spaces is required for Scan Operations access.' } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.scanOperations', + { defaultMessage: 'Scan Operations' } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.scanOperations.description', + { defaultMessage: 'Perform folder scan response actions in the response console.' } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + api: [`${APP_ID}-writeScanOperations`], + id: 'scan_operations_all', + includeIn: 'none', + name: TRANSLATIONS.all, + savedObject: { + all: [], + read: [], + }, + ui: ['writeScanOperations'], + }, + ], + }, + ], +}); + +export const workflowInsightsSubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.workflowInsights.privilegesTooltip', + { defaultMessage: 'All Spaces is required for Automatic Troubleshooting access.' } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.workflowInsights', + { defaultMessage: 'Automatic Troubleshooting' } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.workflowInsights.description', + { defaultMessage: 'Access to the automatic troubleshooting.' } + ), + + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + api: [`${APP_ID}-writeWorkflowInsights`, `${APP_ID}-readWorkflowInsights`], + id: 'workflow_insights_all', + includeIn: 'none', + name: TRANSLATIONS.all, + savedObject: { + all: [], + read: [], + }, + ui: ['writeWorkflowInsights', 'readWorkflowInsights'], + }, + { + api: [`${APP_ID}-readWorkflowInsights`], + id: 'workflow_insights_read', + includeIn: 'none', + name: TRANSLATIONS.read, + savedObject: { + all: [], + read: [], + }, + ui: ['readWorkflowInsights'], + }, + ], + }, + ], +}); + +export const endpointExceptionsSubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointExceptions.privilegesTooltip', + { defaultMessage: 'All Spaces is required for Endpoint Exceptions access.' } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointExceptions', + { defaultMessage: 'Endpoint Exceptions' } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointExceptions.description', + { defaultMessage: 'Manage Endpoint Exceptions.' } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + id: 'endpoint_exceptions_all', + includeIn: 'all', + name: TRANSLATIONS.all, + savedObject: { + all: [], + read: [], + }, + ui: ['showEndpointExceptions', 'crudEndpointExceptions'], + api: [`${APP_ID}-showEndpointExceptions`, `${APP_ID}-crudEndpointExceptions`], + }, + { + id: 'endpoint_exceptions_read', + includeIn: 'read', + name: TRANSLATIONS.read, + savedObject: { + all: [], + read: [], + }, + ui: ['showEndpointExceptions'], + api: [`${APP_ID}-showEndpointExceptions`], + }, + ], + }, + ], +}); + +/** + * Writing global (i.e. not per-policy) Artifacts is gated with `Global Artifact Management: ALL`, starting with `siemV3`. + * + * **Role migration implemented:** + * Users, who have been able to write ANY artifact before, are now granted with this privilege to keep existing behavior. + * - for Trusted Apps, Event Filters, Host Isolation Exceptions, Blocklists: the new privilege is added based on `artifact:ALL` sub-feature privilege + * - for Endpoint Exceptions: + * - on Serverless offering, the new privilege is added for Endpoint Exceptions sub-privilege `ALL`, + * - on ESS offering, there is no EE sub-privilege, so the new privilege is added to `siem|siemV2:ALL|MINIMAL_ALL`, + * as these include the Endpoint Exceptions write privilege + * + */ +export const globalArtifactManagementSubFeature = ( + experimentalFeatures: SecurityFeatureParams['experimentalFeatures'] +): SubFeatureConfig => { + const GLOBAL_ARTIFACT_MANAGEMENT = i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.globalArtifactManagement', + { defaultMessage: 'Global Artifact Management' } + ); + + const COMING_SOON = i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.globalArtifactManagement.comingSoon', + { defaultMessage: '(coming soon)' } + ); + + const name = experimentalFeatures.endpointManagementSpaceAwarenessEnabled + ? GLOBAL_ARTIFACT_MANAGEMENT + : `${GLOBAL_ARTIFACT_MANAGEMENT} ${COMING_SOON}`; + + return { + requireAllSpaces: false, + privilegesTooltip: undefined, + name, + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.globalArtifactManagement.description', + { + defaultMessage: + 'Manage global assignment of endpoint artifacts (e.g., Trusted Applications, Event Filters) ' + + 'across all policies. This privilege controls global assignment rights only; privileges for each ' + + 'artifact type are required for full artifact management.', + } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + api: [`${APP_ID}-writeGlobalArtifacts`], + id: 'global_artifact_management_all', + includeIn: 'none', + name: TRANSLATIONS.all, + savedObject: { + all: [], + read: [], + }, + ui: ['writeGlobalArtifacts'], + }, + ], + }, + ], + }; +}; diff --git a/x-pack/solutions/security/packages/features/src/security/product_feature_config.ts b/x-pack/solutions/security/packages/features/src/security/product_feature_config.ts index 651fb05b5902b..73a95b1ff4f30 100644 --- a/x-pack/solutions/security/packages/features/src/security/product_feature_config.ts +++ b/x-pack/solutions/security/packages/features/src/security/product_feature_config.ts @@ -7,20 +7,9 @@ import { ProductFeatureSecurityKey, SecuritySubFeatureId } from '../product_features_keys'; import { APP_ID } from '../constants'; -import type { DefaultSecurityProductFeaturesConfig } from './types'; +import type { SecurityProductFeaturesConfig } from './types'; -/** - * App features privileges configuration for the Security Solution Kibana Feature app. - * 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 securityDefaultProductFeaturesConfig: DefaultSecurityProductFeaturesConfig = { +export const securityDefaultProductFeaturesConfig: SecurityProductFeaturesConfig = { [ProductFeatureSecurityKey.advancedInsights]: { privileges: { all: { @@ -168,16 +157,14 @@ export const securityDefaultProductFeaturesConfig: DefaultSecurityProductFeature [ProductFeatureSecurityKey.securityWorkflowInsights]: { subFeatureIds: [SecuritySubFeatureId.workflowInsights], }, - // Product features without RBAC - // Endpoint/Osquery PLIs - [ProductFeatureSecurityKey.osqueryAutomatedResponseActions]: {}, - [ProductFeatureSecurityKey.endpointProtectionUpdates]: {}, - [ProductFeatureSecurityKey.endpointAgentTamperProtection]: {}, - [ProductFeatureSecurityKey.endpointCustomNotification]: {}, - [ProductFeatureSecurityKey.externalRuleActions]: {}, - [ProductFeatureSecurityKey.cloudSecurityPosture]: {}, - // Security PLIs - [ProductFeatureSecurityKey.automaticImport]: {}, - [ProductFeatureSecurityKey.prebuiltRuleCustomization]: {}, + [ProductFeatureSecurityKey.endpointArtifactManagement]: { + subFeatureIds: [ + SecuritySubFeatureId.hostIsolationExceptionsBasic, + SecuritySubFeatureId.trustedApplications, + SecuritySubFeatureId.blocklist, + SecuritySubFeatureId.eventFilters, + SecuritySubFeatureId.globalArtifactManagement, + ], + }, }; diff --git a/x-pack/solutions/security/packages/features/src/security/types.ts b/x-pack/solutions/security/packages/features/src/security/types.ts index 7660b02866fc3..33af1d982e62a 100644 --- a/x-pack/solutions/security/packages/features/src/security/types.ts +++ b/x-pack/solutions/security/packages/features/src/security/types.ts @@ -6,7 +6,7 @@ */ import type { ProductFeatureSecurityKey, SecuritySubFeatureId } from '../product_features_keys'; -import type { ProductFeatureKibanaConfig } from '../types'; +import type { ProductFeaturesConfig } from '../types'; export interface SecurityFeatureParams { /** @@ -20,9 +20,7 @@ export interface SecurityFeatureParams { savedObjects: string[]; } -export type DefaultSecurityProductFeaturesConfig = Omit< - Record>, - | ProductFeatureSecurityKey.endpointExceptions - | ProductFeatureSecurityKey.endpointArtifactManagement - // | add not generic security app features here +export type SecurityProductFeaturesConfig = ProductFeaturesConfig< + ProductFeatureSecurityKey, + SecuritySubFeatureId >; diff --git a/x-pack/solutions/security/packages/features/src/security/v1_features/kibana_sub_features.ts b/x-pack/solutions/security/packages/features/src/security/v1_features/kibana_sub_features.ts index 970078c392830..7739b850aff79 100644 --- a/x-pack/solutions/security/packages/features/src/security/v1_features/kibana_sub_features.ts +++ b/x-pack/solutions/security/packages/features/src/security/v1_features/kibana_sub_features.ts @@ -5,739 +5,68 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; import type { SubFeatureConfig } from '@kbn/features-plugin/common'; -import { EXCEPTION_LIST_NAMESPACE_AGNOSTIC } from '@kbn/securitysolution-list-constants'; - +import { SECURITY_FEATURE_ID_V3 } from '../../../constants'; import { SecuritySubFeatureId } from '../../product_features_keys'; -import { APP_ID, SECURITY_FEATURE_ID_V3 } from '../../constants'; import type { SecurityFeatureParams } from '../types'; - -const endpointListSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointList.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Endpoint List access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointList', - { - defaultMessage: 'Endpoint List', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointList.description', - { - defaultMessage: - 'Displays all hosts running Elastic Defend and their relevant integration details.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - replacedBy: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['endpoint_list_all'] }], - api: [`${APP_ID}-writeEndpointList`, `${APP_ID}-readEndpointList`], - id: 'endpoint_list_all', - includeIn: 'none', - name: 'All', - savedObject: { - all: [], - read: [], - }, - ui: ['writeEndpointList', 'readEndpointList'], - }, - { - replacedBy: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['endpoint_list_read'] }], - api: [`${APP_ID}-readEndpointList`], - id: 'endpoint_list_read', - includeIn: 'none', - name: 'Read', - savedObject: { - all: [], - read: [], - }, - ui: ['readEndpointList'], - }, - ], +import type { SubFeatureReplacements } from '../../types'; +import { addSubFeatureReplacements } from '../../utils'; +import { + endpointListSubFeature, + endpointExceptionsSubFeature, + trustedApplicationsSubFeature, + hostIsolationExceptionsBasicSubFeature, + blocklistSubFeature, + eventFiltersSubFeature, + policyManagementSubFeature, + responseActionsHistorySubFeature, + hostIsolationSubFeature, + processOperationsSubFeature, + fileOperationsSubFeature, + executeActionSubFeature, + scanActionSubFeature, +} from '../kibana_sub_features'; + +const replacements: Partial> = { + [SecuritySubFeatureId.endpointList]: [{ feature: SECURITY_FEATURE_ID_V3 }], + [SecuritySubFeatureId.endpointExceptions]: [ + { + feature: SECURITY_FEATURE_ID_V3, + additionalPrivileges: { endpoint_exceptions_all: ['global_artifact_management_all'] }, }, ], -}); - -const trustedApplicationsSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedApplications.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Trusted Applications access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedApplications', - { - defaultMessage: 'Trusted Applications', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedApplications.description', + [SecuritySubFeatureId.trustedApplications]: [ { - defaultMessage: - 'Helps mitigate conflicts with other software, usually other antivirus or endpoint security applications.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - replacedBy: [ - { - feature: SECURITY_FEATURE_ID_V3, - privileges: [ - 'trusted_applications_all', - - // Writing global (not per-policy) Artifacts is gated with Global Artifact Management:ALL starting with siemV3. - // Users who have been able to write ANY Artifact before are now granted with this privilege to keep existing behavior. - 'global_artifact_management_all', - ], - }, - ], - api: [ - 'lists-all', - 'lists-read', - 'lists-summary', - `${APP_ID}-writeTrustedApplications`, - `${APP_ID}-readTrustedApplications`, - `${APP_ID}-writeGlobalArtifacts`, - ], - id: 'trusted_applications_all', - includeIn: 'none', - name: 'All', - savedObject: { - all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC], - read: [], - }, - ui: ['writeTrustedApplications', 'readTrustedApplications'], - }, - { - replacedBy: [ - { feature: SECURITY_FEATURE_ID_V3, privileges: ['trusted_applications_read'] }, - ], - api: ['lists-read', 'lists-summary', `${APP_ID}-readTrustedApplications`], - id: 'trusted_applications_read', - includeIn: 'none', - name: 'Read', - savedObject: { - all: [], - read: [], - }, - ui: ['readTrustedApplications'], - }, - ], + feature: SECURITY_FEATURE_ID_V3, + additionalPrivileges: { trusted_applications_all: ['global_artifact_management_all'] }, }, ], -}); -const hostIsolationExceptionsBasicSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolationExceptions.privilegesTooltip', + [SecuritySubFeatureId.hostIsolationExceptionsBasic]: [ { - defaultMessage: 'All Spaces is required for Host Isolation Exceptions access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolationExceptions', - { - defaultMessage: 'Host Isolation Exceptions', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolationExceptions.description', - { - defaultMessage: - 'Add specific IP addresses that isolated hosts are still allowed to communicate with, even when isolated from the rest of the network.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - replacedBy: [ - { - feature: SECURITY_FEATURE_ID_V3, - privileges: [ - 'host_isolation_exceptions_all', - - // Writing global (not per-policy) Artifacts is gated with Global Artifact Management:ALL starting with siemV3. - // Users who have been able to write ANY Artifact before are now granted with this privilege to keep existing behavior. - 'global_artifact_management_all', - ], - }, - ], - api: [ - 'lists-all', - 'lists-read', - 'lists-summary', - `${APP_ID}-deleteHostIsolationExceptions`, - `${APP_ID}-readHostIsolationExceptions`, - `${APP_ID}-writeGlobalArtifacts`, - ], - id: 'host_isolation_exceptions_all', - includeIn: 'none', - name: 'All', - savedObject: { - all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC], - read: [], - }, - ui: ['readHostIsolationExceptions', 'deleteHostIsolationExceptions'], - }, - { - replacedBy: [ - { feature: SECURITY_FEATURE_ID_V3, privileges: ['host_isolation_exceptions_read'] }, - ], - api: ['lists-read', 'lists-summary', `${APP_ID}-readHostIsolationExceptions`], - id: 'host_isolation_exceptions_read', - includeIn: 'none', - name: 'Read', - savedObject: { - all: [], - read: [], - }, - ui: ['readHostIsolationExceptions'], - }, - ], + feature: SECURITY_FEATURE_ID_V3, + additionalPrivileges: { host_isolation_exceptions_all: ['global_artifact_management_all'] }, }, ], -}); -const blocklistSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.blockList.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Blocklist access.', - } - ), - name: i18n.translate('securitySolutionPackages.features.featureRegistry.subFeatures.blockList', { - defaultMessage: 'Blocklist', - }), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.blockList.description', - { - defaultMessage: - 'Extend Elastic Defend’s protection against malicious processes and protect against potentially harmful applications.', - } - ), - privilegeGroups: [ + [SecuritySubFeatureId.blocklist]: [ { - groupType: 'mutually_exclusive', - privileges: [ - { - replacedBy: [ - { - feature: SECURITY_FEATURE_ID_V3, - privileges: [ - 'blocklist_all', - - // Writing global (not per-policy) Artifacts is gated with Global Artifact Management:ALL starting with siemV3. - // Users who have been able to write ANY Artifact before are now granted with this privilege to keep existing behavior. - 'global_artifact_management_all', - ], - }, - ], - api: [ - 'lists-all', - 'lists-read', - 'lists-summary', - `${APP_ID}-writeBlocklist`, - `${APP_ID}-readBlocklist`, - `${APP_ID}-writeGlobalArtifacts`, - ], - id: 'blocklist_all', - includeIn: 'none', - name: 'All', - savedObject: { - all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC], - read: [], - }, - ui: ['writeBlocklist', 'readBlocklist'], - }, - { - replacedBy: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['blocklist_read'] }], - api: ['lists-read', 'lists-summary', `${APP_ID}-readBlocklist`], - id: 'blocklist_read', - includeIn: 'none', - name: 'Read', - savedObject: { - all: [], - read: [], - }, - ui: ['readBlocklist'], - }, - ], + feature: SECURITY_FEATURE_ID_V3, + additionalPrivileges: { blocklist_all: ['global_artifact_management_all'] }, }, ], -}); -const eventFiltersSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.eventFilters.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Event Filters access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.eventFilters', - { - defaultMessage: 'Event Filters', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.eventFilters.description', + [SecuritySubFeatureId.eventFilters]: [ { - defaultMessage: - 'Filter out endpoint events that you do not need or want stored in Elasticsearch.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - replacedBy: [ - { - feature: SECURITY_FEATURE_ID_V3, - privileges: [ - 'event_filters_all', - - // Writing global (not per-policy) Artifacts is gated with Global Artifact Management:ALL starting with siemV3. - // Users who have been able to write ANY Artifact before are now granted with this privilege to keep existing behavior. - 'global_artifact_management_all', - ], - }, - ], - api: [ - 'lists-all', - 'lists-read', - 'lists-summary', - `${APP_ID}-writeEventFilters`, - `${APP_ID}-readEventFilters`, - `${APP_ID}-writeGlobalArtifacts`, - ], - id: 'event_filters_all', - includeIn: 'none', - name: 'All', - savedObject: { - all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC], - read: [], - }, - ui: ['writeEventFilters', 'readEventFilters'], - }, - { - replacedBy: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['event_filters_read'] }], - api: ['lists-read', 'lists-summary', `${APP_ID}-readEventFilters`], - id: 'event_filters_read', - includeIn: 'none', - name: 'Read', - savedObject: { - all: [], - read: [], - }, - ui: ['readEventFilters'], - }, - ], + feature: SECURITY_FEATURE_ID_V3, + additionalPrivileges: { event_filters_all: ['global_artifact_management_all'] }, }, ], -}); -const policyManagementSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.policyManagement.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Policy Management access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.policyManagement', - { - defaultMessage: 'Elastic Defend Policy Management', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.policyManagement.description', - { - defaultMessage: - 'Access the Elastic Defend integration policy to configure protections, event collection, and advanced policy features.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - replacedBy: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['policy_management_all'] }], - api: [`${APP_ID}-writePolicyManagement`, `${APP_ID}-readPolicyManagement`], - id: 'policy_management_all', - includeIn: 'none', - name: 'All', - savedObject: { - all: ['policy-settings-protection-updates-note'], - read: [], - }, - ui: ['writePolicyManagement', 'readPolicyManagement'], - }, - { - replacedBy: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['policy_management_read'] }], - api: [`${APP_ID}-readPolicyManagement`], - id: 'policy_management_read', - includeIn: 'none', - name: 'Read', - savedObject: { - all: [], - read: ['policy-settings-protection-updates-note'], - }, - ui: ['readPolicyManagement'], - }, - ], - }, - ], -}); - -const responseActionsHistorySubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.responseActionsHistory.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Response Actions History access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.responseActionsHistory', - { - defaultMessage: 'Response Actions History', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.responseActionsHistory.description', - { - defaultMessage: 'Access the history of response actions performed on endpoints.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - replacedBy: [ - { feature: SECURITY_FEATURE_ID_V3, privileges: ['actions_log_management_all'] }, - ], - api: [`${APP_ID}-writeActionsLogManagement`, `${APP_ID}-readActionsLogManagement`], - id: 'actions_log_management_all', - includeIn: 'none', - name: 'All', - savedObject: { - all: [], - read: [], - }, - ui: ['writeActionsLogManagement', 'readActionsLogManagement'], - }, - { - replacedBy: [ - { feature: SECURITY_FEATURE_ID_V3, privileges: ['actions_log_management_read'] }, - ], - api: [`${APP_ID}-readActionsLogManagement`], - id: 'actions_log_management_read', - includeIn: 'none', - name: 'Read', - savedObject: { - all: [], - read: [], - }, - ui: ['readActionsLogManagement'], - }, - ], - }, - ], -}); -const hostIsolationSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolation.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Host Isolation access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolation', - { - defaultMessage: 'Host Isolation', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolation.description', - { defaultMessage: 'Perform the "isolate" and "release" response actions.' } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - replacedBy: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['host_isolation_all'] }], - api: [`${APP_ID}-writeHostIsolationRelease`], - id: 'host_isolation_all', - includeIn: 'none', - name: 'All', - savedObject: { - all: [], - read: [], - }, - ui: ['writeHostIsolationRelease'], - }, - ], - }, - ], -}); - -const processOperationsSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.processOperations.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Process Operations access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.processOperations', - { - defaultMessage: 'Process Operations', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.processOperations.description', - { - defaultMessage: 'Perform process-related response actions in the response console.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - replacedBy: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['process_operations_all'] }], - api: [`${APP_ID}-writeProcessOperations`], - id: 'process_operations_all', - includeIn: 'none', - name: 'All', - savedObject: { - all: [], - read: [], - }, - ui: ['writeProcessOperations'], - }, - ], - }, - ], -}); -const fileOperationsSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.fileOperations.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for File Operations access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.fileOperations', - { - defaultMessage: 'File Operations', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.fileOperations.description', - { - defaultMessage: 'Perform file-related response actions in the response console.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - replacedBy: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['file_operations_all'] }], - api: [`${APP_ID}-writeFileOperations`], - id: 'file_operations_all', - includeIn: 'none', - name: 'All', - savedObject: { - all: [], - read: [], - }, - ui: ['writeFileOperations'], - }, - ], - }, - ], -}); - -// execute operations are not available in 8.7, -// but will be available in 8.8 -const executeActionSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.executeOperations.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Execute Operations access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.executeOperations', - { - defaultMessage: 'Execute Operations', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.executeOperations.description', - { - defaultMessage: 'Perform script execution response actions in the response console.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - replacedBy: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['execute_operations_all'] }], - api: [`${APP_ID}-writeExecuteOperations`], - id: 'execute_operations_all', - includeIn: 'none', - name: 'All', - savedObject: { - all: [], - read: [], - }, - ui: ['writeExecuteOperations'], - }, - ], - }, - ], -}); - -// 8.15 feature -const scanActionSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.scanOperations.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Scan Operations access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.scanOperations', - { - defaultMessage: 'Scan Operations', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.scanOperations.description', - { - defaultMessage: 'Perform folder scan response actions in the response console.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - replacedBy: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['scan_operations_all'] }], - - api: [`${APP_ID}-writeScanOperations`], - id: 'scan_operations_all', - includeIn: 'none', - name: 'All', - savedObject: { - all: [], - read: [], - }, - ui: ['writeScanOperations'], - }, - ], - }, - ], -}); - -const endpointExceptionsSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointExceptions.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Endpoint Exceptions access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointExceptions', - { - defaultMessage: 'Endpoint Exceptions', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointExceptions.description', - { - defaultMessage: 'Manage Endpoint Exceptions.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - replacedBy: [ - { - feature: SECURITY_FEATURE_ID_V3, - privileges: [ - 'endpoint_exceptions_all', - - // Writing global (not per-policy) Artifacts is gated with Global Artifact Management:ALL starting with siemV3. - // Users who have been able to write ANY Artifact before are now granted with this privilege to keep existing behavior. - // This migration is for the serverless offering, where endpoint exception privilege exists. - 'global_artifact_management_all', - ], - }, - ], - id: 'endpoint_exceptions_all', - includeIn: 'all', - name: 'All', - savedObject: { - all: [], - read: [], - }, - ui: ['showEndpointExceptions', 'crudEndpointExceptions'], - api: [ - `${APP_ID}-showEndpointExceptions`, - `${APP_ID}-crudEndpointExceptions`, - `${APP_ID}-writeGlobalArtifacts`, - ], - }, - { - replacedBy: [ - { feature: SECURITY_FEATURE_ID_V3, privileges: ['endpoint_exceptions_read'] }, - ], - id: 'endpoint_exceptions_read', - includeIn: 'read', - name: 'Read', - savedObject: { - all: [], - read: [], - }, - ui: ['showEndpointExceptions'], - api: [`${APP_ID}-showEndpointExceptions`], - }, - ], - }, - ], -}); + [SecuritySubFeatureId.policyManagement]: [{ feature: SECURITY_FEATURE_ID_V3 }], + [SecuritySubFeatureId.responseActionsHistory]: [{ feature: SECURITY_FEATURE_ID_V3 }], + [SecuritySubFeatureId.hostIsolation]: [{ feature: SECURITY_FEATURE_ID_V3 }], + [SecuritySubFeatureId.processOperations]: [{ feature: SECURITY_FEATURE_ID_V3 }], + [SecuritySubFeatureId.fileOperations]: [{ feature: SECURITY_FEATURE_ID_V3 }], + [SecuritySubFeatureId.executeAction]: [{ feature: SECURITY_FEATURE_ID_V3 }], + [SecuritySubFeatureId.scanAction]: [{ feature: SECURITY_FEATURE_ID_V3 }], +}; /** * Sub-features that will always be available for Security @@ -751,61 +80,41 @@ export const getSecurityBaseKibanaSubFeatureIds = ( * Defines all the Security Assistant subFeatures available. * The order of the subFeatures is the order they will be displayed */ - export const getSecuritySubFeaturesMap = ({ experimentalFeatures, }: SecurityFeatureParams): Map => { - const enableSpaceAwarenessIfNeeded = (subFeature: SubFeatureConfig): SubFeatureConfig => { - if (experimentalFeatures.endpointManagementSpaceAwarenessEnabled) { - subFeature.requireAllSpaces = false; - subFeature.privilegesTooltip = undefined; - } - - return subFeature; - }; - const securitySubFeaturesList: Array<[SecuritySubFeatureId, SubFeatureConfig]> = [ - [SecuritySubFeatureId.endpointList, enableSpaceAwarenessIfNeeded(endpointListSubFeature())], - [ - SecuritySubFeatureId.endpointExceptions, - enableSpaceAwarenessIfNeeded(endpointExceptionsSubFeature()), - ], - [ - SecuritySubFeatureId.trustedApplications, - enableSpaceAwarenessIfNeeded(trustedApplicationsSubFeature()), - ], - [ - SecuritySubFeatureId.hostIsolationExceptionsBasic, - enableSpaceAwarenessIfNeeded(hostIsolationExceptionsBasicSubFeature()), - ], - [SecuritySubFeatureId.blocklist, enableSpaceAwarenessIfNeeded(blocklistSubFeature())], - [SecuritySubFeatureId.eventFilters, enableSpaceAwarenessIfNeeded(eventFiltersSubFeature())], - [ - SecuritySubFeatureId.policyManagement, - enableSpaceAwarenessIfNeeded(policyManagementSubFeature()), - ], - [ - SecuritySubFeatureId.responseActionsHistory, - enableSpaceAwarenessIfNeeded(responseActionsHistorySubFeature()), - ], - [SecuritySubFeatureId.hostIsolation, enableSpaceAwarenessIfNeeded(hostIsolationSubFeature())], - [ - SecuritySubFeatureId.processOperations, - enableSpaceAwarenessIfNeeded(processOperationsSubFeature()), - ], - [SecuritySubFeatureId.fileOperations, enableSpaceAwarenessIfNeeded(fileOperationsSubFeature())], - [SecuritySubFeatureId.executeAction, enableSpaceAwarenessIfNeeded(executeActionSubFeature())], - [SecuritySubFeatureId.scanAction, enableSpaceAwarenessIfNeeded(scanActionSubFeature())], + [SecuritySubFeatureId.endpointList, endpointListSubFeature()], + [SecuritySubFeatureId.endpointExceptions, endpointExceptionsSubFeature()], + [SecuritySubFeatureId.trustedApplications, trustedApplicationsSubFeature()], + [SecuritySubFeatureId.hostIsolationExceptionsBasic, hostIsolationExceptionsBasicSubFeature()], + [SecuritySubFeatureId.blocklist, blocklistSubFeature()], + [SecuritySubFeatureId.eventFilters, eventFiltersSubFeature()], + [SecuritySubFeatureId.policyManagement, policyManagementSubFeature()], + [SecuritySubFeatureId.responseActionsHistory, responseActionsHistorySubFeature()], + [SecuritySubFeatureId.hostIsolation, hostIsolationSubFeature()], + [SecuritySubFeatureId.processOperations, processOperationsSubFeature()], + [SecuritySubFeatureId.fileOperations, fileOperationsSubFeature()], + [SecuritySubFeatureId.executeAction, executeActionSubFeature()], + [SecuritySubFeatureId.scanAction, scanActionSubFeature()], ]; - // Use the following code to add feature based on feature flag - // if (experimentalFeatures.featureFlagName) { - // securitySubFeaturesList.push([SecuritySubFeatureId.featureId, featureSubFeature]); - // } - const securitySubFeaturesMap = new Map( - securitySubFeaturesList - ); + securitySubFeaturesList.map(([id, originalSubFeature]) => { + let subFeature = originalSubFeature; + + const featureReplacements = replacements[id]; + if (featureReplacements) { + subFeature = addSubFeatureReplacements(subFeature, featureReplacements); + } + // If the feature is space-aware, we need to set false to the requireAllSpaces flag and remove the privilegesTooltip + if (experimentalFeatures.endpointManagementSpaceAwarenessEnabled) { + subFeature = { ...subFeature, requireAllSpaces: false, privilegesTooltip: undefined }; + } + + return [id, subFeature]; + }) + ); return Object.freeze(securitySubFeaturesMap); }; diff --git a/x-pack/solutions/security/packages/features/src/security/v1_features/product_feature_config.ts b/x-pack/solutions/security/packages/features/src/security/v1_features/product_feature_config.ts new file mode 100644 index 0000000000000..8fcb014c69c80 --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/security/v1_features/product_feature_config.ts @@ -0,0 +1,32 @@ +/* + * 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 { ProductFeatureSecurityKey } from '../../product_features_keys'; +import { APP_ID } from '../../constants'; +import type { SecurityProductFeaturesConfig } from '../types'; +import { extendProductFeatureConfigs } from '../../utils'; +import { securityDefaultProductFeaturesConfig } from '../product_feature_config'; + +export const securityV1ProductFeaturesConfig: SecurityProductFeaturesConfig = + extendProductFeatureConfigs(securityDefaultProductFeaturesConfig, { + // Add the global artifact management API privilege to the all privileges of siem and siemV2 features for backwards compatibility + // The siemV3 adds the global artifact management API privilege as a sub-feature. + // This config adds the new global artifact management API privilege to old versions so we have only one way of authorizing this functionality. + // No need to add the ui capability here, since they are automatically added by the Kibana features framework via the `replacedBy` field. + [ProductFeatureSecurityKey.endpointArtifactManagement]: { + // Adds the action to the top-level feature "all" privilege + privileges: { all: { api: [`${APP_ID}-writeGlobalArtifacts`] } }, + // Some sub-features were also allowing this action, they need to be extended as well (the top-level feature may be in "read" level). + subFeaturesPrivileges: [ + { id: 'endpoint_exceptions_all', api: [`${APP_ID}-writeGlobalArtifacts`] }, + { id: 'trusted_applications_all', api: [`${APP_ID}-writeGlobalArtifacts`] }, + { id: 'host_isolation_exceptions_all', api: [`${APP_ID}-writeGlobalArtifacts`] }, + { id: 'blocklist_all', api: [`${APP_ID}-writeGlobalArtifacts`] }, + { id: 'event_filters_all', api: [`${APP_ID}-writeGlobalArtifacts`] }, + ], + }, + }); diff --git a/x-pack/solutions/security/packages/features/src/security/v2_features/kibana_sub_features.ts b/x-pack/solutions/security/packages/features/src/security/v2_features/kibana_sub_features.ts index de61b640a8e4b..9642ea7d9dfe4 100644 --- a/x-pack/solutions/security/packages/features/src/security/v2_features/kibana_sub_features.ts +++ b/x-pack/solutions/security/packages/features/src/security/v2_features/kibana_sub_features.ts @@ -5,848 +5,73 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; import type { SubFeatureConfig } from '@kbn/features-plugin/common'; -import { EXCEPTION_LIST_NAMESPACE_AGNOSTIC } from '@kbn/securitysolution-list-constants'; import { SecuritySubFeatureId } from '../../product_features_keys'; -import { APP_ID, SECURITY_FEATURE_ID_V3 } from '../../constants'; +import { SECURITY_FEATURE_ID_V3 } from '../../constants'; import type { SecurityFeatureParams } from '../types'; - -const TRANSLATIONS = Object.freeze({ - all: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.allPrivilegeName', - { - defaultMessage: 'All', - } - ), - read: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.readPrivilegeName', - { - defaultMessage: 'Read', - } - ), -}); - -const endpointListSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointList.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Endpoint List access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointList', - { - defaultMessage: 'Endpoint List', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointList.description', - { - defaultMessage: - 'Displays all hosts running Elastic Defend and their relevant integration details.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - replacedBy: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['endpoint_list_all'] }], - api: [`${APP_ID}-writeEndpointList`, `${APP_ID}-readEndpointList`], - id: 'endpoint_list_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: [], - read: [], - }, - ui: ['writeEndpointList', 'readEndpointList'], - }, - { - replacedBy: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['endpoint_list_read'] }], - api: [`${APP_ID}-readEndpointList`], - id: 'endpoint_list_read', - includeIn: 'none', - name: TRANSLATIONS.read, - savedObject: { - all: [], - read: [], - }, - ui: ['readEndpointList'], - }, - ], +import { + endpointListSubFeature, + endpointExceptionsSubFeature, + globalArtifactManagementSubFeature, + trustedApplicationsSubFeature, + hostIsolationExceptionsBasicSubFeature, + blocklistSubFeature, + eventFiltersSubFeature, + policyManagementSubFeature, + responseActionsHistorySubFeature, + hostIsolationSubFeature, + processOperationsSubFeature, + fileOperationsSubFeature, + executeActionSubFeature, + scanActionSubFeature, + workflowInsightsSubFeature, +} from '../kibana_sub_features'; +import type { SubFeatureReplacements } from '../../types'; +import { addSubFeatureReplacements } from '../../utils'; + +const replacements: Partial> = { + [SecuritySubFeatureId.endpointList]: [{ feature: SECURITY_FEATURE_ID_V3 }], + [SecuritySubFeatureId.workflowInsights]: [{ feature: SECURITY_FEATURE_ID_V3 }], + [SecuritySubFeatureId.endpointExceptions]: [ + { + feature: SECURITY_FEATURE_ID_V3, + additionalPrivileges: { endpoint_exceptions_all: ['global_artifact_management_all'] }, }, ], -}); - -const trustedApplicationsSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedApplications.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Trusted Applications access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedApplications', - { - defaultMessage: 'Trusted Applications', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedApplications.description', - { - defaultMessage: - 'Helps mitigate conflicts with other software, usually other antivirus or endpoint security applications.', - } - ), - privilegeGroups: [ + [SecuritySubFeatureId.globalArtifactManagement]: [{ feature: SECURITY_FEATURE_ID_V3 }], + [SecuritySubFeatureId.trustedApplications]: [ { - groupType: 'mutually_exclusive', - privileges: [ - { - replacedBy: [ - { - feature: SECURITY_FEATURE_ID_V3, - privileges: [ - 'trusted_applications_all', - - // Writing global (not per-policy) Artifacts is gated with Global Artifact Management:ALL starting with siemV3. - // Users who have been able to write ANY Artifact before are now granted with this privilege to keep existing behavior. - 'global_artifact_management_all', - ], - }, - ], - api: [ - 'lists-all', - 'lists-read', - 'lists-summary', - `${APP_ID}-writeTrustedApplications`, - `${APP_ID}-readTrustedApplications`, - `${APP_ID}-writeGlobalArtifacts`, - ], - id: 'trusted_applications_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC], - read: [], - }, - ui: ['writeTrustedApplications', 'readTrustedApplications'], - }, - { - replacedBy: [ - { feature: SECURITY_FEATURE_ID_V3, privileges: ['trusted_applications_read'] }, - ], - api: ['lists-read', 'lists-summary', `${APP_ID}-readTrustedApplications`], - id: 'trusted_applications_read', - includeIn: 'none', - name: TRANSLATIONS.read, - savedObject: { - all: [], - read: [], - }, - ui: ['readTrustedApplications'], - }, - ], + feature: SECURITY_FEATURE_ID_V3, + additionalPrivileges: { trusted_applications_all: ['global_artifact_management_all'] }, }, ], -}); -const hostIsolationExceptionsBasicSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolationExceptions.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Host Isolation Exceptions access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolationExceptions', - { - defaultMessage: 'Host Isolation Exceptions', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolationExceptions.description', + [SecuritySubFeatureId.hostIsolationExceptionsBasic]: [ { - defaultMessage: - 'Add specific IP addresses that isolated hosts are still allowed to communicate with, even when isolated from the rest of the network.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - replacedBy: [ - { - feature: SECURITY_FEATURE_ID_V3, - privileges: [ - 'host_isolation_exceptions_all', - - // Writing global (not per-policy) Artifacts is gated with Global Artifact Management:ALL starting with siemV3. - // Users who have been able to write ANY Artifact before are now granted with this privilege to keep existing behavior. - 'global_artifact_management_all', - ], - }, - ], - api: [ - 'lists-all', - 'lists-read', - 'lists-summary', - `${APP_ID}-deleteHostIsolationExceptions`, - `${APP_ID}-readHostIsolationExceptions`, - `${APP_ID}-writeGlobalArtifacts`, - ], - id: 'host_isolation_exceptions_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC], - read: [], - }, - ui: ['readHostIsolationExceptions', 'deleteHostIsolationExceptions'], - }, - { - replacedBy: [ - { feature: SECURITY_FEATURE_ID_V3, privileges: ['host_isolation_exceptions_read'] }, - ], - api: ['lists-read', 'lists-summary', `${APP_ID}-readHostIsolationExceptions`], - id: 'host_isolation_exceptions_read', - includeIn: 'none', - name: TRANSLATIONS.read, - savedObject: { - all: [], - read: [], - }, - ui: ['readHostIsolationExceptions'], - }, - ], + feature: SECURITY_FEATURE_ID_V3, + additionalPrivileges: { host_isolation_exceptions_all: ['global_artifact_management_all'] }, }, ], -}); -const blocklistSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.blockList.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Blocklist access.', - } - ), - name: i18n.translate('securitySolutionPackages.features.featureRegistry.subFeatures.blockList', { - defaultMessage: 'Blocklist', - }), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.blockList.description', - { - defaultMessage: - 'Extend Elastic Defend’s protection against malicious processes and protect against potentially harmful applications.', - } - ), - privilegeGroups: [ + [SecuritySubFeatureId.blocklist]: [ { - groupType: 'mutually_exclusive', - privileges: [ - { - replacedBy: [ - { - feature: SECURITY_FEATURE_ID_V3, - privileges: [ - 'blocklist_all', - - // Writing global (not per-policy) Artifacts is gated with Global Artifact Management:ALL starting with siemV3. - // Users who have been able to write ANY Artifact before are now granted with this privilege to keep existing behavior. - 'global_artifact_management_all', - ], - }, - ], - api: [ - 'lists-all', - 'lists-read', - 'lists-summary', - `${APP_ID}-writeBlocklist`, - `${APP_ID}-readBlocklist`, - `${APP_ID}-writeGlobalArtifacts`, - ], - id: 'blocklist_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC], - read: [], - }, - ui: ['writeBlocklist', 'readBlocklist'], - }, - { - replacedBy: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['blocklist_read'] }], - api: ['lists-read', 'lists-summary', `${APP_ID}-readBlocklist`], - id: 'blocklist_read', - includeIn: 'none', - name: TRANSLATIONS.read, - savedObject: { - all: [], - read: [], - }, - ui: ['readBlocklist'], - }, - ], + feature: SECURITY_FEATURE_ID_V3, + additionalPrivileges: { blocklist_all: ['global_artifact_management_all'] }, }, ], -}); -const eventFiltersSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.eventFilters.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Event Filters access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.eventFilters', - { - defaultMessage: 'Event Filters', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.eventFilters.description', + [SecuritySubFeatureId.eventFilters]: [ { - defaultMessage: - 'Filter out endpoint events that you do not need or want stored in Elasticsearch.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - replacedBy: [ - { - feature: SECURITY_FEATURE_ID_V3, - privileges: [ - 'event_filters_all', - - // Writing global (not per-policy) Artifacts is gated with Global Artifact Management:ALL starting with siemV3. - // Users who have been able to write ANY Artifact before are now granted with this privilege to keep existing behavior. - 'global_artifact_management_all', - ], - }, - ], - api: [ - 'lists-all', - 'lists-read', - 'lists-summary', - `${APP_ID}-writeEventFilters`, - `${APP_ID}-readEventFilters`, - `${APP_ID}-writeGlobalArtifacts`, - ], - id: 'event_filters_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC], - read: [], - }, - ui: ['writeEventFilters', 'readEventFilters'], - }, - { - replacedBy: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['event_filters_read'] }], - api: ['lists-read', 'lists-summary', `${APP_ID}-readEventFilters`], - id: 'event_filters_read', - includeIn: 'none', - name: TRANSLATIONS.read, - savedObject: { - all: [], - read: [], - }, - ui: ['readEventFilters'], - }, - ], + feature: SECURITY_FEATURE_ID_V3, + additionalPrivileges: { event_filters_all: ['global_artifact_management_all'] }, }, ], -}); -const policyManagementSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.policyManagement.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Policy Management access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.policyManagement', - { - defaultMessage: 'Elastic Defend Policy Management', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.policyManagement.description', - { - defaultMessage: - 'Access the Elastic Defend integration policy to configure protections, event collection, and advanced policy features.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - replacedBy: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['policy_management_all'] }], - api: [`${APP_ID}-writePolicyManagement`, `${APP_ID}-readPolicyManagement`], - id: 'policy_management_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: ['policy-settings-protection-updates-note'], - read: [], - }, - ui: ['writePolicyManagement', 'readPolicyManagement'], - }, - { - replacedBy: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['policy_management_read'] }], - api: [`${APP_ID}-readPolicyManagement`], - id: 'policy_management_read', - includeIn: 'none', - name: TRANSLATIONS.read, - savedObject: { - all: [], - read: ['policy-settings-protection-updates-note'], - }, - ui: ['readPolicyManagement'], - }, - ], - }, - ], -}); - -const responseActionsHistorySubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.responseActionsHistory.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Response Actions History access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.responseActionsHistory', - { - defaultMessage: 'Response Actions History', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.responseActionsHistory.description', - { - defaultMessage: 'Access the history of response actions performed on endpoints.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - replacedBy: [ - { feature: SECURITY_FEATURE_ID_V3, privileges: ['actions_log_management_all'] }, - ], - api: [`${APP_ID}-writeActionsLogManagement`, `${APP_ID}-readActionsLogManagement`], - id: 'actions_log_management_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: [], - read: [], - }, - ui: ['writeActionsLogManagement', 'readActionsLogManagement'], - }, - { - replacedBy: [ - { feature: SECURITY_FEATURE_ID_V3, privileges: ['actions_log_management_read'] }, - ], - api: [`${APP_ID}-readActionsLogManagement`], - id: 'actions_log_management_read', - includeIn: 'none', - name: TRANSLATIONS.read, - savedObject: { - all: [], - read: [], - }, - ui: ['readActionsLogManagement'], - }, - ], - }, - ], -}); -const hostIsolationSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolation.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Host Isolation access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolation', - { - defaultMessage: 'Host Isolation', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolation.description', - { defaultMessage: 'Perform the "isolate" and "release" response actions.' } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - replacedBy: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['host_isolation_all'] }], - api: [`${APP_ID}-writeHostIsolationRelease`], - id: 'host_isolation_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: [], - read: [], - }, - ui: ['writeHostIsolationRelease'], - }, - ], - }, - ], -}); - -const processOperationsSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.processOperations.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Process Operations access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.processOperations', - { - defaultMessage: 'Process Operations', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.processOperations.description', - { - defaultMessage: 'Perform process-related response actions in the response console.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - replacedBy: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['process_operations_all'] }], - api: [`${APP_ID}-writeProcessOperations`], - id: 'process_operations_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: [], - read: [], - }, - ui: ['writeProcessOperations'], - }, - ], - }, - ], -}); -const fileOperationsSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.fileOperations.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for File Operations access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.fileOperations', - { - defaultMessage: 'File Operations', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.fileOperations.description', - { - defaultMessage: 'Perform file-related response actions in the response console.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - replacedBy: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['file_operations_all'] }], - api: [`${APP_ID}-writeFileOperations`], - id: 'file_operations_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: [], - read: [], - }, - ui: ['writeFileOperations'], - }, - ], - }, - ], -}); - -// execute operations are not available in 8.7, -// but will be available in 8.8 -const executeActionSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.executeOperations.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Execute Operations access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.executeOperations', - { - defaultMessage: 'Execute Operations', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.executeOperations.description', - { - defaultMessage: 'Perform script execution response actions in the response console.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - replacedBy: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['execute_operations_all'] }], - api: [`${APP_ID}-writeExecuteOperations`], - id: 'execute_operations_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: [], - read: [], - }, - ui: ['writeExecuteOperations'], - }, - ], - }, - ], -}); - -// 8.15 feature -const scanActionSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.scanOperations.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Scan Operations access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.scanOperations', - { - defaultMessage: 'Scan Operations', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.scanOperations.description', - { - defaultMessage: 'Perform folder scan response actions in the response console.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - replacedBy: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['scan_operations_all'] }], - api: [`${APP_ID}-writeScanOperations`], - id: 'scan_operations_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: [], - read: [], - }, - ui: ['writeScanOperations'], - }, - ], - }, - ], -}); - -const workflowInsightsSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.workflowInsights.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Automatic Troubleshooting access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.workflowInsights', - { - defaultMessage: 'Automatic Troubleshooting', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.workflowInsights.description', - { - defaultMessage: 'Access to the automatic troubleshooting.', - } - ), - - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - replacedBy: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['workflow_insights_all'] }], - api: [`${APP_ID}-writeWorkflowInsights`, `${APP_ID}-readWorkflowInsights`], - id: 'workflow_insights_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: [], - read: [], - }, - ui: ['writeWorkflowInsights', 'readWorkflowInsights'], - }, - { - replacedBy: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['workflow_insights_read'] }], - api: [`${APP_ID}-readWorkflowInsights`], - id: 'workflow_insights_read', - includeIn: 'none', - name: TRANSLATIONS.read, - savedObject: { - all: [], - read: [], - }, - ui: ['readWorkflowInsights'], - }, - ], - }, - ], -}); - -const endpointExceptionsSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointExceptions.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Endpoint Exceptions access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointExceptions', - { - defaultMessage: 'Endpoint Exceptions', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointExceptions.description', - { - defaultMessage: 'Manage Endpoint Exceptions.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - replacedBy: [ - { - feature: SECURITY_FEATURE_ID_V3, - privileges: [ - 'endpoint_exceptions_all', - - // Writing global (not per-policy) Artifacts is gated with Global Artifact Management:ALL starting with siemV3. - // Users who have been able to write ANY Artifact before are now granted with this privilege to keep existing behavior. - // This migration is for the serverless offering, where endpoint exception privilege exists. - 'global_artifact_management_all', - ], - }, - ], - id: 'endpoint_exceptions_all', - includeIn: 'all', - name: TRANSLATIONS.all, - savedObject: { - all: [], - read: [], - }, - ui: ['showEndpointExceptions', 'crudEndpointExceptions'], - api: [ - `${APP_ID}-showEndpointExceptions`, - `${APP_ID}-crudEndpointExceptions`, - `${APP_ID}-writeGlobalArtifacts`, - ], - }, - { - replacedBy: [ - { feature: SECURITY_FEATURE_ID_V3, privileges: ['endpoint_exceptions_read'] }, - ], - id: 'endpoint_exceptions_read', - includeIn: 'read', - name: TRANSLATIONS.read, - savedObject: { - all: [], - read: [], - }, - ui: ['showEndpointExceptions'], - api: [`${APP_ID}-showEndpointExceptions`], - }, - ], - }, - ], -}); - -const globalArtifactManagementSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: false, - privilegesTooltip: undefined, - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.globalArtifactManagement', - { - defaultMessage: 'Global Artifact Management', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.globalArtifactManagement.description', - { - defaultMessage: - 'Manage global assignment of endpoint artifacts (e.g., Trusted Applications, Event Filters) ' + - 'across all policies. This privilege controls global assignment rights only; privileges for each ' + - 'artifact type are required for full artifact management.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - replacedBy: [ - { feature: SECURITY_FEATURE_ID_V3, privileges: ['global_artifact_management_all'] }, - ], - api: [`${APP_ID}-writeGlobalArtifacts`], - id: 'global_artifact_management_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: [], - read: [], - }, - ui: ['writeGlobalArtifacts'], - }, - ], - }, - ], -}); + [SecuritySubFeatureId.policyManagement]: [{ feature: SECURITY_FEATURE_ID_V3 }], + [SecuritySubFeatureId.responseActionsHistory]: [{ feature: SECURITY_FEATURE_ID_V3 }], + [SecuritySubFeatureId.hostIsolation]: [{ feature: SECURITY_FEATURE_ID_V3 }], + [SecuritySubFeatureId.processOperations]: [{ feature: SECURITY_FEATURE_ID_V3 }], + [SecuritySubFeatureId.fileOperations]: [{ feature: SECURITY_FEATURE_ID_V3 }], + [SecuritySubFeatureId.executeAction]: [{ feature: SECURITY_FEATURE_ID_V3 }], + [SecuritySubFeatureId.scanAction]: [{ feature: SECURITY_FEATURE_ID_V3 }], +}; /** * Sub-features that will always be available for Security @@ -860,80 +85,55 @@ export const getSecurityV2BaseKibanaSubFeatureIds = ( * Defines all the Security Assistant subFeatures available. * The order of the subFeatures is the order they will be displayed */ - export const getSecurityV2SubFeaturesMap = ({ experimentalFeatures, }: SecurityFeatureParams): Map => { - const enableSpaceAwarenessIfNeeded = (subFeature: SubFeatureConfig): SubFeatureConfig => { - if (experimentalFeatures.endpointManagementSpaceAwarenessEnabled) { - subFeature.requireAllSpaces = false; - subFeature.privilegesTooltip = undefined; - } - - return subFeature; - }; - const securitySubFeaturesList: Array<[SecuritySubFeatureId, SubFeatureConfig]> = [ - [SecuritySubFeatureId.endpointList, enableSpaceAwarenessIfNeeded(endpointListSubFeature())], + [SecuritySubFeatureId.endpointList, endpointListSubFeature()], + [SecuritySubFeatureId.workflowInsights, workflowInsightsSubFeature()], + [SecuritySubFeatureId.endpointExceptions, endpointExceptionsSubFeature()], [ - SecuritySubFeatureId.endpointExceptions, - enableSpaceAwarenessIfNeeded(endpointExceptionsSubFeature()), + SecuritySubFeatureId.globalArtifactManagement, + globalArtifactManagementSubFeature(experimentalFeatures), ], + [SecuritySubFeatureId.trustedApplications, trustedApplicationsSubFeature()], + [SecuritySubFeatureId.hostIsolationExceptionsBasic, hostIsolationExceptionsBasicSubFeature()], + [SecuritySubFeatureId.blocklist, blocklistSubFeature()], + [SecuritySubFeatureId.eventFilters, eventFiltersSubFeature()], + [SecuritySubFeatureId.policyManagement, policyManagementSubFeature()], + [SecuritySubFeatureId.responseActionsHistory, responseActionsHistorySubFeature()], + [SecuritySubFeatureId.hostIsolation, hostIsolationSubFeature()], + [SecuritySubFeatureId.processOperations, processOperationsSubFeature()], + [SecuritySubFeatureId.fileOperations, fileOperationsSubFeature()], + [SecuritySubFeatureId.executeAction, executeActionSubFeature()], + [SecuritySubFeatureId.scanAction, scanActionSubFeature()], + ]; - ...((experimentalFeatures.endpointManagementSpaceAwarenessEnabled - ? [ - [ - SecuritySubFeatureId.globalArtifactManagement, - enableSpaceAwarenessIfNeeded(globalArtifactManagementSubFeature()), - ], - ] - : []) as Array<[SecuritySubFeatureId, SubFeatureConfig]>), + const securitySubFeaturesMap = new Map( + securitySubFeaturesList.map(([id, originalSubFeature]) => { + let subFeature = originalSubFeature; - [ - SecuritySubFeatureId.trustedApplications, - enableSpaceAwarenessIfNeeded(trustedApplicationsSubFeature()), - ], - [ - SecuritySubFeatureId.hostIsolationExceptionsBasic, - enableSpaceAwarenessIfNeeded(hostIsolationExceptionsBasicSubFeature()), - ], - [SecuritySubFeatureId.blocklist, enableSpaceAwarenessIfNeeded(blocklistSubFeature())], - [SecuritySubFeatureId.eventFilters, enableSpaceAwarenessIfNeeded(eventFiltersSubFeature())], + const featureReplacements = replacements[id]; + if (featureReplacements) { + subFeature = addSubFeatureReplacements(subFeature, featureReplacements); + } - [ - SecuritySubFeatureId.policyManagement, - enableSpaceAwarenessIfNeeded(policyManagementSubFeature()), - ], - [ - SecuritySubFeatureId.responseActionsHistory, - enableSpaceAwarenessIfNeeded(responseActionsHistorySubFeature()), - ], - [SecuritySubFeatureId.hostIsolation, enableSpaceAwarenessIfNeeded(hostIsolationSubFeature())], - [ - SecuritySubFeatureId.processOperations, - enableSpaceAwarenessIfNeeded(processOperationsSubFeature()), - ], - [SecuritySubFeatureId.fileOperations, enableSpaceAwarenessIfNeeded(fileOperationsSubFeature())], - [SecuritySubFeatureId.executeAction, enableSpaceAwarenessIfNeeded(executeActionSubFeature())], - [SecuritySubFeatureId.scanAction, enableSpaceAwarenessIfNeeded(scanActionSubFeature())], - ]; + // If the feature is space-aware, we need to set false to the requireAllSpaces flag and remove the privilegesTooltip + if (experimentalFeatures.endpointManagementSpaceAwarenessEnabled) { + subFeature = { ...subFeature, requireAllSpaces: false, privilegesTooltip: undefined }; + } - // Use the following code to add feature based on feature flag - // if (experimentalFeatures.featureFlagName) { - // securitySubFeaturesList.push([SecuritySubFeatureId.featureId, featureSubFeature]); - // } + return [id, subFeature]; + }) + ); - if (experimentalFeatures.defendInsights) { - // place with other All/Read/None options - securitySubFeaturesList.splice(1, 0, [ - SecuritySubFeatureId.workflowInsights, - enableSpaceAwarenessIfNeeded(workflowInsightsSubFeature()), - ]); + // Remove disabled experimental features + if (!experimentalFeatures.endpointManagementSpaceAwarenessEnabled) { + securitySubFeaturesMap.delete(SecuritySubFeatureId.globalArtifactManagement); + } + if (!experimentalFeatures.defendInsights) { + securitySubFeaturesMap.delete(SecuritySubFeatureId.workflowInsights); } - - const securitySubFeaturesMap = new Map( - securitySubFeaturesList - ); return Object.freeze(securitySubFeaturesMap); }; diff --git a/x-pack/solutions/security/packages/features/src/security/v2_features/product_feature_config.ts b/x-pack/solutions/security/packages/features/src/security/v2_features/product_feature_config.ts new file mode 100644 index 0000000000000..3ae4a96ec332a --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/security/v2_features/product_feature_config.ts @@ -0,0 +1,32 @@ +/* + * 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 { ProductFeatureSecurityKey } from '../../product_features_keys'; +import { APP_ID } from '../../constants'; +import type { SecurityProductFeaturesConfig } from '../types'; +import { extendProductFeatureConfigs } from '../../utils'; +import { securityDefaultProductFeaturesConfig } from '../product_feature_config'; + +export const securityV2ProductFeaturesConfig: SecurityProductFeaturesConfig = + extendProductFeatureConfigs(securityDefaultProductFeaturesConfig, { + // Add the global artifact management API privilege to the all privileges of siem and siemV2 features for backwards compatibility + // The siemV3 adds the global artifact management API privilege as a sub-feature. + // This config adds the new global artifact management API privilege to old versions so we have only one way of authorizing this functionality. + // No need to add the ui capability here, since they are automatically added by the Kibana features framework via the `replacedBy` field. + [ProductFeatureSecurityKey.endpointArtifactManagement]: { + // Adds the action to the top-level feature "all" privilege + privileges: { all: { api: [`${APP_ID}-writeGlobalArtifacts`] } }, + // Some sub-features were also allowing this action, they need to be extended as well (the top-level feature may be in "read" level). + subFeaturesPrivileges: [ + { id: 'endpoint_exceptions_all', api: [`${APP_ID}-writeGlobalArtifacts`] }, + { id: 'trusted_applications_all', api: [`${APP_ID}-writeGlobalArtifacts`] }, + { id: 'host_isolation_exceptions_all', api: [`${APP_ID}-writeGlobalArtifacts`] }, + { id: 'blocklist_all', api: [`${APP_ID}-writeGlobalArtifacts`] }, + { id: 'event_filters_all', api: [`${APP_ID}-writeGlobalArtifacts`] }, + ], + }, + }); diff --git a/x-pack/solutions/security/packages/features/src/security/v3_features/kibana_sub_features.ts b/x-pack/solutions/security/packages/features/src/security/v3_features/kibana_sub_features.ts index 4cb39c324bb9c..6174a5c9ab25b 100644 --- a/x-pack/solutions/security/packages/features/src/security/v3_features/kibana_sub_features.ts +++ b/x-pack/solutions/security/packages/features/src/security/v3_features/kibana_sub_features.ts @@ -5,832 +5,27 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; import type { SubFeatureConfig } from '@kbn/features-plugin/common'; -import { EXCEPTION_LIST_NAMESPACE_AGNOSTIC } from '@kbn/securitysolution-list-constants'; - import { SecuritySubFeatureId } from '../../product_features_keys'; -import { APP_ID } from '../../constants'; import type { SecurityFeatureParams } from '../types'; - -const TRANSLATIONS = Object.freeze({ - all: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.allPrivilegeName', - { - defaultMessage: 'All', - } - ), - read: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.readPrivilegeName', - { - defaultMessage: 'Read', - } - ), -}); - -const endpointListSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointList.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Endpoint List access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointList', - { - defaultMessage: 'Endpoint List', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointList.description', - { - defaultMessage: - 'Displays all hosts running Elastic Defend and their relevant integration details.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - api: [`${APP_ID}-writeEndpointList`, `${APP_ID}-readEndpointList`], - id: 'endpoint_list_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: [], - read: [], - }, - ui: ['writeEndpointList', 'readEndpointList'], - }, - { - api: [`${APP_ID}-readEndpointList`], - id: 'endpoint_list_read', - includeIn: 'none', - name: TRANSLATIONS.read, - savedObject: { - all: [], - read: [], - }, - ui: ['readEndpointList'], - }, - ], - }, - ], -}); - -const trustedApplicationsSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedApplications.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Trusted Applications access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedApplications', - { - defaultMessage: 'Trusted Applications', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedApplications.description', - { - defaultMessage: - 'Helps mitigate conflicts with other software, usually other antivirus or endpoint security applications.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - api: [ - 'lists-all', - 'lists-read', - 'lists-summary', - `${APP_ID}-writeTrustedApplications`, - `${APP_ID}-readTrustedApplications`, - ], - id: 'trusted_applications_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC], - read: [], - }, - ui: ['writeTrustedApplications', 'readTrustedApplications'], - }, - { - api: ['lists-read', 'lists-summary', `${APP_ID}-readTrustedApplications`], - id: 'trusted_applications_read', - includeIn: 'none', - name: TRANSLATIONS.read, - savedObject: { - all: [], - read: [], - }, - ui: ['readTrustedApplications'], - }, - ], - }, - ], -}); - -const trustedDevicesSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedDevices.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Trusted Devices access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedDevices', - { - defaultMessage: 'Trusted Devices', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedDevices.description', - { - defaultMessage: - 'Allows management of trusted USB and external devices that bypass device control protections.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - api: [ - 'lists-all', - 'lists-read', - 'lists-summary', - `${APP_ID}-writeTrustedDevices`, - `${APP_ID}-readTrustedDevices`, - ], - id: 'trusted_devices_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC], - read: [], - }, - ui: ['writeTrustedDevices', 'readTrustedDevices'], - }, - { - api: ['lists-read', 'lists-summary', `${APP_ID}-readTrustedDevices`], - id: 'trusted_devices_read', - includeIn: 'none', - name: TRANSLATIONS.read, - savedObject: { - all: [], - read: [], - }, - ui: ['readTrustedDevices'], - }, - ], - }, - ], -}); - -const hostIsolationExceptionsBasicSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolationExceptions.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Host Isolation Exceptions access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolationExceptions', - { - defaultMessage: 'Host Isolation Exceptions', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolationExceptions.description', - { - defaultMessage: - 'Add specific IP addresses that isolated hosts are still allowed to communicate with, even when isolated from the rest of the network.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - api: [ - 'lists-all', - 'lists-read', - 'lists-summary', - `${APP_ID}-deleteHostIsolationExceptions`, - `${APP_ID}-readHostIsolationExceptions`, - ], - id: 'host_isolation_exceptions_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC], - read: [], - }, - ui: ['readHostIsolationExceptions', 'deleteHostIsolationExceptions'], - }, - { - api: ['lists-read', 'lists-summary', `${APP_ID}-readHostIsolationExceptions`], - id: 'host_isolation_exceptions_read', - includeIn: 'none', - name: TRANSLATIONS.read, - savedObject: { - all: [], - read: [], - }, - ui: ['readHostIsolationExceptions'], - }, - ], - }, - ], -}); -const blocklistSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.blockList.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Blocklist access.', - } - ), - name: i18n.translate('securitySolutionPackages.features.featureRegistry.subFeatures.blockList', { - defaultMessage: 'Blocklist', - }), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.blockList.description', - { - defaultMessage: - 'Extend Elastic Defend’s protection against malicious processes and protect against potentially harmful applications.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - api: [ - 'lists-all', - 'lists-read', - 'lists-summary', - `${APP_ID}-writeBlocklist`, - `${APP_ID}-readBlocklist`, - ], - id: 'blocklist_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC], - read: [], - }, - ui: ['writeBlocklist', 'readBlocklist'], - }, - { - api: ['lists-read', 'lists-summary', `${APP_ID}-readBlocklist`], - id: 'blocklist_read', - includeIn: 'none', - name: TRANSLATIONS.read, - savedObject: { - all: [], - read: [], - }, - ui: ['readBlocklist'], - }, - ], - }, - ], -}); -const eventFiltersSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.eventFilters.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Event Filters access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.eventFilters', - { - defaultMessage: 'Event Filters', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.eventFilters.description', - { - defaultMessage: - 'Filter out endpoint events that you do not need or want stored in Elasticsearch.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - api: [ - 'lists-all', - 'lists-read', - 'lists-summary', - `${APP_ID}-writeEventFilters`, - `${APP_ID}-readEventFilters`, - ], - id: 'event_filters_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC], - read: [], - }, - ui: ['writeEventFilters', 'readEventFilters'], - }, - { - api: ['lists-read', 'lists-summary', `${APP_ID}-readEventFilters`], - id: 'event_filters_read', - includeIn: 'none', - name: TRANSLATIONS.read, - savedObject: { - all: [], - read: [], - }, - ui: ['readEventFilters'], - }, - ], - }, - ], -}); -const policyManagementSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.policyManagement.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Policy Management access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.policyManagement', - { - defaultMessage: 'Elastic Defend Policy Management', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.policyManagement.description', - { - defaultMessage: - 'Access the Elastic Defend integration policy to configure protections, event collection, and advanced policy features.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - api: [`${APP_ID}-writePolicyManagement`, `${APP_ID}-readPolicyManagement`], - id: 'policy_management_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: ['policy-settings-protection-updates-note'], - read: [], - }, - ui: ['writePolicyManagement', 'readPolicyManagement'], - }, - { - api: [`${APP_ID}-readPolicyManagement`], - id: 'policy_management_read', - includeIn: 'none', - name: TRANSLATIONS.read, - savedObject: { - all: [], - read: ['policy-settings-protection-updates-note'], - }, - ui: ['readPolicyManagement'], - }, - ], - }, - ], -}); - -const responseActionsHistorySubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.responseActionsHistory.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Response Actions History access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.responseActionsHistory', - { - defaultMessage: 'Response Actions History', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.responseActionsHistory.description', - { - defaultMessage: 'Access the history of response actions performed on endpoints.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - api: [`${APP_ID}-writeActionsLogManagement`, `${APP_ID}-readActionsLogManagement`], - id: 'actions_log_management_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: [], - read: [], - }, - ui: ['writeActionsLogManagement', 'readActionsLogManagement'], - }, - { - api: [`${APP_ID}-readActionsLogManagement`], - id: 'actions_log_management_read', - includeIn: 'none', - name: TRANSLATIONS.read, - savedObject: { - all: [], - read: [], - }, - ui: ['readActionsLogManagement'], - }, - ], - }, - ], -}); -const hostIsolationSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolation.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Host Isolation access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolation', - { - defaultMessage: 'Host Isolation', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolation.description', - { defaultMessage: 'Perform the "isolate" and "release" response actions.' } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - api: [`${APP_ID}-writeHostIsolationRelease`], - id: 'host_isolation_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: [], - read: [], - }, - ui: ['writeHostIsolationRelease'], - }, - ], - }, - ], -}); - -const processOperationsSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.processOperations.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Process Operations access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.processOperations', - { - defaultMessage: 'Process Operations', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.processOperations.description', - { - defaultMessage: 'Perform process-related response actions in the response console.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - api: [`${APP_ID}-writeProcessOperations`], - id: 'process_operations_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: [], - read: [], - }, - ui: ['writeProcessOperations'], - }, - ], - }, - ], -}); -const fileOperationsSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.fileOperations.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for File Operations access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.fileOperations', - { - defaultMessage: 'File Operations', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.fileOperations.description', - { - defaultMessage: 'Perform file-related response actions in the response console.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - api: [`${APP_ID}-writeFileOperations`], - id: 'file_operations_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: [], - read: [], - }, - ui: ['writeFileOperations'], - }, - ], - }, - ], -}); - -// execute operations are not available in 8.7, -// but will be available in 8.8 -const executeActionSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.executeOperations.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Execute Operations access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.executeOperations', - { - defaultMessage: 'Execute Operations', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.executeOperations.description', - { - defaultMessage: 'Perform script execution response actions in the response console.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - api: [`${APP_ID}-writeExecuteOperations`], - id: 'execute_operations_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: [], - read: [], - }, - ui: ['writeExecuteOperations'], - }, - ], - }, - ], -}); - -// 8.15 feature -const scanActionSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.scanOperations.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Scan Operations access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.scanOperations', - { - defaultMessage: 'Scan Operations', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.scanOperations.description', - { - defaultMessage: 'Perform folder scan response actions in the response console.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - api: [`${APP_ID}-writeScanOperations`], - id: 'scan_operations_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: [], - read: [], - }, - ui: ['writeScanOperations'], - }, - ], - }, - ], -}); - -const workflowInsightsSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.workflowInsights.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Automatic Troubleshooting access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.workflowInsights', - { - defaultMessage: 'Automatic Troubleshooting', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.workflowInsights.description', - { - defaultMessage: 'Access to the automatic troubleshooting.', - } - ), - - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - api: [`${APP_ID}-writeWorkflowInsights`, `${APP_ID}-readWorkflowInsights`], - id: 'workflow_insights_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: [], - read: [], - }, - ui: ['writeWorkflowInsights', 'readWorkflowInsights'], - }, - { - api: [`${APP_ID}-readWorkflowInsights`], - id: 'workflow_insights_read', - includeIn: 'none', - name: TRANSLATIONS.read, - savedObject: { - all: [], - read: [], - }, - ui: ['readWorkflowInsights'], - }, - ], - }, - ], -}); - -const endpointExceptionsSubFeature = (): SubFeatureConfig => ({ - requireAllSpaces: true, - privilegesTooltip: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointExceptions.privilegesTooltip', - { - defaultMessage: 'All Spaces is required for Endpoint Exceptions access.', - } - ), - name: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointExceptions', - { - defaultMessage: 'Endpoint Exceptions', - } - ), - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointExceptions.description', - { - defaultMessage: 'Manage Endpoint Exceptions.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - id: 'endpoint_exceptions_all', - includeIn: 'all', - name: TRANSLATIONS.all, - savedObject: { - all: [], - read: [], - }, - ui: ['showEndpointExceptions', 'crudEndpointExceptions'], - api: [`${APP_ID}-showEndpointExceptions`, `${APP_ID}-crudEndpointExceptions`], - }, - { - id: 'endpoint_exceptions_read', - includeIn: 'read', - name: TRANSLATIONS.read, - savedObject: { - all: [], - read: [], - }, - ui: ['showEndpointExceptions'], - api: [`${APP_ID}-showEndpointExceptions`], - }, - ], - }, - ], -}); - -/** - * Writing global (i.e. not per-policy) Artifacts is gated with `Global Artifact Management: ALL`, starting with `siemV3`. - * - * **Role migration implemented:** - * Users, who have been able to write ANY artifact before, are now granted with this privilege to keep existing behavior. - * - for Trusted Apps, Event Filters, Host Isolation Exceptions, Blocklists: the new privilege is added based on `artifact:ALL` sub-feature privilege - * - for Endpoint Exceptions: - * - on Serverless offering, the new privilege is added for Endpoint Exceptions sub-privilege `ALL`, - * - on ESS offering, there is no EE sub-privilege, so the new privilege is added to `siem|siemV2:ALL|MINIMAL_ALL`, - * as these include the Endpoint Exceptions write privilege - * - */ -const globalArtifactManagementSubFeature = ( - experimentalFeatures: SecurityFeatureParams['experimentalFeatures'] -): SubFeatureConfig => { - const GLOBAL_ARTIFACT_MANAGEMENT = i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.globalArtifactManagement', - { defaultMessage: 'Global Artifact Management' } - ); - - const COMING_SOON = i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.globalArtifactManagement.comingSoon', - { defaultMessage: '(coming soon)' } - ); - - const name = experimentalFeatures.endpointManagementSpaceAwarenessEnabled - ? GLOBAL_ARTIFACT_MANAGEMENT - : `${GLOBAL_ARTIFACT_MANAGEMENT} ${COMING_SOON}`; - - return { - requireAllSpaces: false, - privilegesTooltip: undefined, - name, - description: i18n.translate( - 'securitySolutionPackages.features.featureRegistry.subFeatures.globalArtifactManagement.description', - { - defaultMessage: - 'Manage global assignment of endpoint artifacts (e.g., Trusted Applications, Event Filters) ' + - 'across all policies. This privilege controls global assignment rights only; privileges for each ' + - 'artifact type are required for full artifact management.', - } - ), - privilegeGroups: [ - { - groupType: 'mutually_exclusive', - privileges: [ - { - api: [`${APP_ID}-writeGlobalArtifacts`], - id: 'global_artifact_management_all', - includeIn: 'none', - name: TRANSLATIONS.all, - savedObject: { - all: [], - read: [], - }, - ui: ['writeGlobalArtifacts'], - }, - ], - }, - ], - }; -}; +import { + endpointListSubFeature, + endpointExceptionsSubFeature, + globalArtifactManagementSubFeature, + trustedApplicationsSubFeature, + hostIsolationExceptionsBasicSubFeature, + blocklistSubFeature, + eventFiltersSubFeature, + policyManagementSubFeature, + responseActionsHistorySubFeature, + hostIsolationSubFeature, + processOperationsSubFeature, + fileOperationsSubFeature, + executeActionSubFeature, + scanActionSubFeature, + workflowInsightsSubFeature, + trustedDevicesSubFeature, +} from '../kibana_sub_features'; /** * Sub-features that will always be available for Security @@ -844,84 +39,51 @@ export const getSecurityV3BaseKibanaSubFeatureIds = ( * Defines all the Security Assistant subFeatures available. * The order of the subFeatures is the order they will be displayed */ - export const getSecurityV3SubFeaturesMap = ({ experimentalFeatures, }: SecurityFeatureParams): Map => { - const enableSpaceAwarenessIfNeeded = (subFeature: SubFeatureConfig): SubFeatureConfig => { - if (experimentalFeatures.endpointManagementSpaceAwarenessEnabled) { - subFeature.requireAllSpaces = false; - subFeature.privilegesTooltip = undefined; - } - - return subFeature; - }; - const securitySubFeaturesList: Array<[SecuritySubFeatureId, SubFeatureConfig]> = [ - [SecuritySubFeatureId.endpointList, enableSpaceAwarenessIfNeeded(endpointListSubFeature())], - [ - SecuritySubFeatureId.endpointExceptions, - enableSpaceAwarenessIfNeeded(endpointExceptionsSubFeature()), - ], - + [SecuritySubFeatureId.endpointList, endpointListSubFeature()], + [SecuritySubFeatureId.workflowInsights, workflowInsightsSubFeature()], + [SecuritySubFeatureId.endpointExceptions, endpointExceptionsSubFeature()], [ SecuritySubFeatureId.globalArtifactManagement, - enableSpaceAwarenessIfNeeded(globalArtifactManagementSubFeature(experimentalFeatures)), - ], - - [ - SecuritySubFeatureId.trustedApplications, - enableSpaceAwarenessIfNeeded(trustedApplicationsSubFeature()), - ], - ...((experimentalFeatures.trustedDevices - ? [ - [ - SecuritySubFeatureId.trustedDevices, - enableSpaceAwarenessIfNeeded(trustedDevicesSubFeature()), - ], - ] - : []) as Array<[SecuritySubFeatureId, SubFeatureConfig]>), - [ - SecuritySubFeatureId.hostIsolationExceptionsBasic, - enableSpaceAwarenessIfNeeded(hostIsolationExceptionsBasicSubFeature()), - ], - [SecuritySubFeatureId.blocklist, enableSpaceAwarenessIfNeeded(blocklistSubFeature())], - [SecuritySubFeatureId.eventFilters, enableSpaceAwarenessIfNeeded(eventFiltersSubFeature())], - - [ - SecuritySubFeatureId.policyManagement, - enableSpaceAwarenessIfNeeded(policyManagementSubFeature()), - ], - [ - SecuritySubFeatureId.responseActionsHistory, - enableSpaceAwarenessIfNeeded(responseActionsHistorySubFeature()), + globalArtifactManagementSubFeature(experimentalFeatures), ], - [SecuritySubFeatureId.hostIsolation, enableSpaceAwarenessIfNeeded(hostIsolationSubFeature())], - [ - SecuritySubFeatureId.processOperations, - enableSpaceAwarenessIfNeeded(processOperationsSubFeature()), - ], - [SecuritySubFeatureId.fileOperations, enableSpaceAwarenessIfNeeded(fileOperationsSubFeature())], - [SecuritySubFeatureId.executeAction, enableSpaceAwarenessIfNeeded(executeActionSubFeature())], - [SecuritySubFeatureId.scanAction, enableSpaceAwarenessIfNeeded(scanActionSubFeature())], + [SecuritySubFeatureId.trustedApplications, trustedApplicationsSubFeature()], + [SecuritySubFeatureId.trustedDevices, trustedDevicesSubFeature()], + [SecuritySubFeatureId.hostIsolationExceptionsBasic, hostIsolationExceptionsBasicSubFeature()], + [SecuritySubFeatureId.blocklist, blocklistSubFeature()], + [SecuritySubFeatureId.eventFilters, eventFiltersSubFeature()], + [SecuritySubFeatureId.policyManagement, policyManagementSubFeature()], + [SecuritySubFeatureId.responseActionsHistory, responseActionsHistorySubFeature()], + [SecuritySubFeatureId.hostIsolation, hostIsolationSubFeature()], + [SecuritySubFeatureId.processOperations, processOperationsSubFeature()], + [SecuritySubFeatureId.fileOperations, fileOperationsSubFeature()], + [SecuritySubFeatureId.executeAction, executeActionSubFeature()], + [SecuritySubFeatureId.scanAction, scanActionSubFeature()], ]; - // Use the following code to add feature based on feature flag - // if (experimentalFeatures.featureFlagName) { - // securitySubFeaturesList.push([SecuritySubFeatureId.featureId, featureSubFeature]); - // } + const securitySubFeaturesMap = new Map( + securitySubFeaturesList.map(([id, originalSubFeature]) => { + let subFeature = originalSubFeature; - if (experimentalFeatures.defendInsights) { - // place with other All/Read/None options - securitySubFeaturesList.splice(1, 0, [ - SecuritySubFeatureId.workflowInsights, - enableSpaceAwarenessIfNeeded(workflowInsightsSubFeature()), - ]); - } + // If the feature is space-aware, we need to set false to the requireAllSpaces flag and remove the privilegesTooltip + if (experimentalFeatures.endpointManagementSpaceAwarenessEnabled) { + subFeature = { ...subFeature, requireAllSpaces: false, privilegesTooltip: undefined }; + } - const securitySubFeaturesMap = new Map( - securitySubFeaturesList + return [id, subFeature]; + }) ); + // Remove disabled experimental features + if (!experimentalFeatures.defendInsights) { + securitySubFeaturesMap.delete(SecuritySubFeatureId.workflowInsights); + } + if (!experimentalFeatures.trustedDevices) { + securitySubFeaturesMap.delete(SecuritySubFeatureId.trustedDevices); + } + return Object.freeze(securitySubFeaturesMap); }; 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 index 0fa2e897bb05a..7d3671592d8ae 100644 --- a/x-pack/solutions/security/packages/features/src/siem_migrations/index.ts +++ b/x-pack/solutions/security/packages/features/src/siem_migrations/index.ts @@ -7,9 +7,9 @@ import { getSiemMigrationsBaseKibanaFeature } from './kibana_features'; import type { ProductFeatureParams } from '../types'; +import { siemMigrationsProductFeaturesConfig } from './product_feature_config'; export const getSiemMigrationsFeature = (): ProductFeatureParams => ({ baseKibanaFeature: getSiemMigrationsBaseKibanaFeature(), - baseKibanaSubFeatureIds: [], - subFeaturesMap: new Map(), + productFeatureConfig: siemMigrationsProductFeaturesConfig, }); 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 9db1be79228cf..3fe8338cfbfef 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 @@ -7,28 +7,16 @@ import { SIEM_MIGRATIONS_API_ACTION_ALL } from '../actions'; import { ProductFeatureSiemMigrationsKey } from '../product_features_keys'; -import type { ProductFeatureKibanaConfig } from '../types'; +import type { ProductFeaturesConfig } from '../types'; -/** - * App features privileges configuration for the Attack discovery feature. - * These are the configs that are shared between both offering types (ess and serverless). - * They can be extended on each offering plugin to register privileges using different way on each offering type. - * - * Privileges can be added in different ways: - * - `privileges`: the privileges that will be added directly into the main Security feature. - * - `subFeatureIds`: the ids of the sub-features that will be added into the Security subFeatures entry. - * - `subFeaturesPrivileges`: the privileges that will be added into the existing Security subFeature with the privilege `id` specified. - */ -export const siemMigrationsDefaultProductFeaturesConfig: Record< - ProductFeatureSiemMigrationsKey, - ProductFeatureKibanaConfig -> = { - [ProductFeatureSiemMigrationsKey.siemMigrations]: { - privileges: { - all: { - api: [SIEM_MIGRATIONS_API_ACTION_ALL], - ui: ['all'], +export const siemMigrationsProductFeaturesConfig: ProductFeaturesConfig = + { + [ProductFeatureSiemMigrationsKey.siemMigrations]: { + privileges: { + all: { + api: [SIEM_MIGRATIONS_API_ACTION_ALL], + ui: ['all'], + }, }, }, - }, -}; + }; diff --git a/x-pack/solutions/security/packages/features/src/timeline/index.ts b/x-pack/solutions/security/packages/features/src/timeline/index.ts index 62042881ec6f2..b508f9da9dff8 100644 --- a/x-pack/solutions/security/packages/features/src/timeline/index.ts +++ b/x-pack/solutions/security/packages/features/src/timeline/index.ts @@ -8,9 +8,9 @@ import { getTimelineBaseKibanaFeature } from './kibana_features'; import type { ProductFeatureParams } from '../types'; import type { SecurityFeatureParams } from '../security/types'; +import { timelineProductFeaturesConfig } from './product_feature_config'; export const getTimelineFeature = (params: SecurityFeatureParams): ProductFeatureParams => ({ baseKibanaFeature: getTimelineBaseKibanaFeature(params), - baseKibanaSubFeatureIds: [], - subFeaturesMap: new Map(), + productFeatureConfig: timelineProductFeaturesConfig, }); diff --git a/x-pack/solutions/security/packages/features/src/timeline/product_feature_config.ts b/x-pack/solutions/security/packages/features/src/timeline/product_feature_config.ts index dd442014bf6fe..9042292f6f5ff 100644 --- a/x-pack/solutions/security/packages/features/src/timeline/product_feature_config.ts +++ b/x-pack/solutions/security/packages/features/src/timeline/product_feature_config.ts @@ -5,24 +5,11 @@ * 2.0. */ -import { ProductFeatureTimelineFeatureKey } from '../product_features_keys'; -import type { ProductFeatureKibanaConfig } from '../types'; +import { ProductFeatureTimelineKey } from '../product_features_keys'; +import type { ProductFeaturesConfig } from '../types'; -/** - * App features privileges configuration for the timeline 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 timelineDefaultProductFeaturesConfig: Record< - ProductFeatureTimelineFeatureKey, - ProductFeatureKibanaConfig -> = { - [ProductFeatureTimelineFeatureKey.timeline]: { +export const timelineProductFeaturesConfig: ProductFeaturesConfig = { + [ProductFeatureTimelineKey.timeline]: { privileges: { all: { api: ['timeline_read', 'timeline_write'], diff --git a/x-pack/solutions/security/packages/features/src/types.ts b/x-pack/solutions/security/packages/features/src/types.ts index 1d7dd25455bf6..959528dd5361a 100644 --- a/x-pack/solutions/security/packages/features/src/types.ts +++ b/x-pack/solutions/security/packages/features/src/types.ts @@ -10,7 +10,7 @@ import type { SubFeatureConfig, SubFeaturePrivilegeConfig, } from '@kbn/features-plugin/common'; -import type { RecursivePartial } from '@kbn/utility-types'; +import type { RecursivePartial, RecursiveWritable } from '@kbn/utility-types'; import type { ProductFeatureAssistantKey, ProductFeatureAttackDiscoveryKey, @@ -21,8 +21,8 @@ import type { CasesSubFeatureId, SecuritySubFeatureId, ProductFeatureSiemMigrationsKey, - ProductFeatureTimelineFeatureKey, - ProductFeatureNotesFeatureKey, + ProductFeatureTimelineKey, + ProductFeatureNotesKey, } from './product_features_keys'; export type { ProductFeatureKeyType }; @@ -31,73 +31,117 @@ export type ProductFeatureKeys = ProductFeatureKeyType[]; // Features types export type BaseKibanaFeatureConfig = Omit; export type SubFeaturesPrivileges = RecursivePartial; + +export type MutableKibanaFeatureConfig = RecursiveWritable; +export type MutableSubFeatureConfig = RecursiveWritable; + +export type FeatureConfigModifier = (config: MutableKibanaFeatureConfig) => void; + export type ProductFeatureKibanaConfig = RecursivePartial & { + /** + * List of sub-feature IDs that will be added into the Security subFeatures entry. + */ subFeatureIds?: T[]; + + /** + * List of additional privileges that will be merged into existing Security subFeature with the privilege `id` specified. + */ subFeaturesPrivileges?: SubFeaturesPrivileges[]; - /** An option for product features to modify the base kibana feature. + /** + * Functions to apply free modifications to the resulting Kibana feature config when a specific ProductFeatureKey is enabled. + * The `kibanaFeatureConfig` object received is a deep copy of the original configuration, it can be mutated safely. + * The modifications are applied after merging the configs of all the ProductFeatureKeys, it includes the final `subFeatures` array. * - * @param baseFeatureConfig - * @returns modified baseFeatureConfig + * @param kibanaFeatureConfig to be mutated + * @returns void */ - baseFeatureConfigModifier?: ( - baseFeatureConfig: BaseKibanaFeatureConfig - ) => BaseKibanaFeatureConfig; + featureConfigModifiers?: FeatureConfigModifier[]; }; -export type ProductFeaturesConfig = Map< - ProductFeatureKeyType, - ProductFeatureKibanaConfig ->; -export type ProductFeaturesSecurityConfig = Map< +/** + * App features privileges configuration for the Security Solution Kibana Feature app. + * 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. + * - `featureConfigModifiers`: functions to apply free modifications to the resulting Kibana feature config when a specific ProductFeatureKey is enabled. + */ +export type ProductFeaturesConfig< + K extends ProductFeatureKeyType = ProductFeatureKeyType, + T extends string = string +> = Partial>>; + +export type SecurityProductFeaturesConfig = ProductFeaturesConfig< ProductFeatureSecurityKey, - ProductFeatureKibanaConfig + SecuritySubFeatureId >; -export type ProductFeaturesCasesConfig = Map< +export type CasesProductFeaturesConfig = ProductFeaturesConfig< ProductFeatureCasesKey, - ProductFeatureKibanaConfig + CasesSubFeatureId >; - -export type ProductFeaturesAssistantConfig = Map< +export type AssistantProductFeaturesConfig = ProductFeaturesConfig< ProductFeatureAssistantKey, - ProductFeatureKibanaConfig + AssistantSubFeatureId >; +export type AttackDiscoveryProductFeaturesConfig = + ProductFeaturesConfig; +export type TimelineProductFeaturesConfig = ProductFeaturesConfig; +export type NotesProductFeaturesConfig = ProductFeaturesConfig; +export type SiemMigrationsProductFeaturesConfig = + ProductFeaturesConfig; -export type ProductFeaturesAttackDiscoveryConfig = Map< - ProductFeatureAttackDiscoveryKey, - ProductFeatureKibanaConfig ->; +export type AppSubFeaturesMap = Map; -export type ProductFeaturesTimelineConfig = Map< - ProductFeatureTimelineFeatureKey, - ProductFeatureKibanaConfig ->; +export interface ProductFeatureParams< + K extends ProductFeatureKeyType = ProductFeatureKeyType, + S extends string = string +> { + baseKibanaFeature: BaseKibanaFeatureConfig; + baseKibanaSubFeatureIds?: S[]; + subFeaturesMap?: AppSubFeaturesMap; + productFeatureConfig?: ProductFeaturesConfig; +} -export type ProductFeaturesNotesConfig = Map< - ProductFeatureNotesFeatureKey, - ProductFeatureKibanaConfig ->; +export interface ConfigExtensions { + /** The `allVersions` is used to extend all the versions of the feature group */ + allVersions: C; + /** The `version` object indexed by the feature `id` */ + version: Record; +} -export type ProductFeaturesSiemMigrationsConfig = Map< - ProductFeatureSiemMigrationsKey, - ProductFeatureKibanaConfig ->; +interface ProductFeatureConfigExtensions { + security: ConfigExtensions; + cases: ConfigExtensions; + securityAssistant: ConfigExtensions; + attackDiscovery: ConfigExtensions; + timeline: ConfigExtensions; + notes: ConfigExtensions; + siemMigrations: ConfigExtensions; +} -export type AppSubFeaturesMap = Map; +export type ProductFeaturesConfiguratorExtensions = Partial; -export interface ProductFeatureParams { - baseKibanaFeature: BaseKibanaFeatureConfig; - baseKibanaSubFeatureIds: T[]; - subFeaturesMap: AppSubFeaturesMap; +export interface ProductFeaturesConfigurator { + enabledProductFeatureKeys: ProductFeatureKeyType[]; + extensions?: ProductFeaturesConfiguratorExtensions; } -export interface ProductFeaturesConfigurator { - security: () => ProductFeaturesConfig; - cases: () => ProductFeaturesConfig; - securityAssistant: () => ProductFeaturesConfig; - attackDiscovery: () => ProductFeaturesConfig; - timeline: () => ProductFeaturesConfig; - notes: () => ProductFeaturesConfig; - siemMigrations: () => ProductFeaturesConfig; +export type ProductFeatureGroup = keyof ProductFeatureConfigExtensions; + +export interface SubFeatureReplacement { + /** The (top-level) feature id that will replace the sub-feature */ + feature: string; + /** If true, the additional privileges will be added to the replacedBy array */ + additionalPrivileges?: Record; + /** If true, the current privilege id will not be copied to the replacedBy array. + * This is useful for discontinuing a sub-feature privilege, e.g. when splitting + * the sub-feature into two or just removing it. + */ + removeOriginalPrivileges?: boolean; } +export type SubFeatureReplacements = SubFeatureReplacement[]; diff --git a/x-pack/solutions/security/packages/features/src/utils/index.ts b/x-pack/solutions/security/packages/features/src/utils/index.ts new file mode 100644 index 0000000000000..cc3aa3a44941b --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/utils/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './product_feature_config'; +export * from './sub_features'; diff --git a/x-pack/solutions/security/packages/features/src/utils/product_feature_config.test.ts b/x-pack/solutions/security/packages/features/src/utils/product_feature_config.test.ts new file mode 100644 index 0000000000000..8a37bf31e829a --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/utils/product_feature_config.test.ts @@ -0,0 +1,254 @@ +/* + * 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 { ProductFeatureKeyType } from '../product_features_keys'; +import type { ProductFeaturesConfig } from '../types'; +import { featureConfigMerger, extendProductFeatureConfigs } from './product_feature_config'; + +const feature1 = 'feature1' as ProductFeatureKeyType; +const feature2 = 'feature2' as ProductFeatureKeyType; + +describe('product_feature_config', () => { + describe('featureConfigMerger', () => { + it('merges arrays with unique values', () => { + const array1 = [1, 2, 3]; + const array2 = [3, 4, 5]; + + const result = featureConfigMerger(array1, array2); + + expect(result).toEqual([1, 2, 3, 4, 5]); + }); + + it('returns undefined for non-array values', () => { + const obj1 = { key: 'value1' }; + const obj2 = { key: 'value2' }; + + const result = featureConfigMerger(obj1, obj2); + + expect(result).toBeUndefined(); + }); + + it('handles empty arrays', () => { + const array1: number[] = []; + const array2 = [1, 2, 3]; + + const result = featureConfigMerger(array1, array2); + + expect(result).toEqual([1, 2, 3]); + }); + + it('handles arrays with objects', () => { + const array1 = [{ id: 1 }, { id: 2 }]; + const array2 = [{ id: 2 }, { id: 3 }]; + + const result = featureConfigMerger(array1, array2); + + // Note: Since uniq does shallow comparison, objects with same structure but different references are considered unique + expect(result).toEqual([{ id: 1 }, { id: 2 }, { id: 2 }, { id: 3 }]); + }); + }); + + describe('extendProductFeatureConfigs', () => { + it('merges concatenates arrays', () => { + const config1: ProductFeaturesConfig = { + [feature1]: { + subFeatureIds: ['subFeature1', 'subFeature2'], + subFeaturesPrivileges: [ + { + id: 'privilege1', + ui: ['ui1', 'ui2'], + }, + ], + }, + }; + + const config2: ProductFeaturesConfig = { + [feature1]: { + subFeatureIds: ['subFeature2', 'subFeature3'], + subFeaturesPrivileges: [ + { + id: 'privilege2', + ui: ['ui3'], + }, + ], + }, + [feature2]: { + subFeatureIds: ['subFeature4'], + }, + }; + + const result = extendProductFeatureConfigs(config1, config2); + + expect(result).toEqual({ + [feature1]: { + subFeatureIds: ['subFeature1', 'subFeature2', 'subFeature3'], + subFeaturesPrivileges: [ + { + id: 'privilege1', + ui: ['ui1', 'ui2'], + }, + { + id: 'privilege2', + ui: ['ui3'], + }, + ], + }, + [feature2]: { + subFeatureIds: ['subFeature4'], + }, + }); + }); + + it('discards duplicates inside arrays', () => { + const config1: ProductFeaturesConfig = { + [feature1]: { + privileges: { + all: { + ui: ['ui1', 'ui2'], + }, + }, + }, + }; + + const config2: ProductFeaturesConfig = { + [feature1]: { + privileges: { + all: { + ui: ['ui2', 'ui3'], + }, + }, + }, + }; + + const result = extendProductFeatureConfigs(config1, config2); + + expect(result).toEqual({ + [feature1]: { + privileges: { + all: { + ui: ['ui1', 'ui2', 'ui3'], + }, + }, + }, + }); + }); + + it('returns empty object when no configs are provided', () => { + const result = extendProductFeatureConfigs(); + + expect(result).toEqual({}); + }); + + it('handles nested objects and arrays', () => { + const config1: ProductFeaturesConfig = { + [feature1]: { + app: ['app1'], + catalogue: ['catalogue1'], + privileges: { + all: { + savedObject: { + all: ['so1', 'so2'], + read: ['so1'], + }, + ui: ['ui1'], + }, + }, + }, + }; + + const config2: ProductFeaturesConfig = { + [feature1]: { + app: ['app2'], + catalogue: ['catalogue1', 'catalogue2'], + privileges: { + all: { + savedObject: { + all: ['so2', 'so3'], + read: ['so2'], + }, + ui: ['ui1', 'ui2'], + }, + }, + }, + }; + + const result = extendProductFeatureConfigs(config1, config2); + + expect(result).toEqual({ + [feature1]: { + app: ['app1', 'app2'], + catalogue: ['catalogue1', 'catalogue2'], + privileges: { + all: { + savedObject: { + all: ['so1', 'so2', 'so3'], + read: ['so1', 'so2'], + }, + ui: ['ui1', 'ui2'], + }, + }, + }, + }); + }); + + it('does not mutate original configs', () => { + const config1 = { + [feature1]: { + subFeatureIds: ['subFeature1'], + }, + }; + + const config2 = { + [feature1]: { + subFeatureIds: ['subFeature2'], + }, + }; + + const originalConfig1 = { ...config1 }; + const originalConfig2 = { ...config2 }; + + extendProductFeatureConfigs(config1, config2); + + expect(config1).toEqual(originalConfig1); + expect(config2).toEqual(originalConfig2); + }); + + it('handles multiple configs', () => { + const config1 = { + [feature1]: { + subFeatureIds: ['subFeature1'], + }, + }; + + const config2 = { + [feature1]: { + subFeatureIds: ['subFeature2'], + }, + }; + + const config3 = { + [feature1]: { + subFeatureIds: ['subFeature3'], + }, + [feature2]: { + subFeatureIds: ['subFeature4'], + }, + }; + + const result = extendProductFeatureConfigs(config1, config2, config3); + + expect(result).toEqual({ + [feature1]: { + subFeatureIds: ['subFeature1', 'subFeature2', 'subFeature3'], + }, + [feature2]: { + subFeatureIds: ['subFeature4'], + }, + }); + }); + }); +}); diff --git a/x-pack/solutions/security/packages/features/src/utils/product_feature_config.ts b/x-pack/solutions/security/packages/features/src/utils/product_feature_config.ts new file mode 100644 index 0000000000000..35e002e7d834a --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/utils/product_feature_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 { mergeWith, uniq } from 'lodash'; +import type { ProductFeatureKeyType, ProductFeaturesConfig } from '../types'; + +/** + * Custom merge function for product feature configs. To be used with `mergeWith`. + * It merges arrays by removing duplicates by shallow comparison and extends other properties. + * It does not mutate the original objects. + * @param objValue - The value from the first object + * @param srcValue - The value from the second object + * @returns The merged value + */ +export const featureConfigMerger = (objValue: unknown, srcValue: unknown) => { + if (Array.isArray(objValue) && Array.isArray(srcValue)) { + return uniq(objValue.concat(srcValue)); + } + return undefined; // Use default merge behavior for other types +}; + +/** + * Extends multiple ProductFeaturesConfig objects into a single one. + * It merges arrays by removing duplicates and keeps the rest of the properties as is. + * It does not mutate the original objects. + * + * @param productFeatureConfigs - The product feature configs to merge + * @returns A single extended ProductFeaturesConfig object + */ +export const extendProductFeatureConfigs = < + K extends ProductFeatureKeyType, + S extends string = string +>( + ...productFeatureConfigs: Array> +): ProductFeaturesConfig => { + return mergeWith({}, ...productFeatureConfigs, featureConfigMerger); +}; diff --git a/x-pack/solutions/security/packages/features/src/utils/sub_features.test.ts b/x-pack/solutions/security/packages/features/src/utils/sub_features.test.ts new file mode 100644 index 0000000000000..34d938059096e --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/utils/sub_features.test.ts @@ -0,0 +1,278 @@ +/* + * 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 { SubFeatureConfig } from '@kbn/features-plugin/common'; +import type { SubFeatureReplacement } from '../types'; +import { addAllSubFeatureReplacements, addSubFeatureReplacements } from './sub_features'; + +describe('sub_features', () => { + describe('addSubFeatureReplacements', () => { + const mockSubFeature: SubFeatureConfig = { + name: 'Test SubFeature', + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + id: 'privilege1', + name: 'Test Privilege 1', + includeIn: 'read', + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + { + id: 'privilege2', + name: 'Test Privilege 2', + includeIn: 'all', + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + ], + }, + ], + }; + + it('returns the original subFeature if no replacements are provided', () => { + const result = addSubFeatureReplacements(mockSubFeature, []); + expect(result).toBe(mockSubFeature); + }); + + it('adds replacements to all privileges within the subFeature', () => { + const replacements: SubFeatureReplacement[] = [ + { + feature: 'replacementFeature', + removeOriginalPrivileges: false, + }, + ]; + + const result = addSubFeatureReplacements(mockSubFeature, replacements); + + // Should not mutate original + expect(mockSubFeature.privilegeGroups[0].privileges[0].replacedBy).toBeUndefined(); + + // Should add replacedBy to all privileges + expect(result.privilegeGroups[0].privileges[0].replacedBy).toEqual([ + { feature: 'replacementFeature', privileges: ['privilege1'] }, + ]); + expect(result.privilegeGroups[0].privileges[1].replacedBy).toEqual([ + { feature: 'replacementFeature', privileges: ['privilege2'] }, + ]); + }); + + it('does not copy privilege IDs when removeOriginalPrivileges is true', () => { + const replacements: SubFeatureReplacement[] = [ + { + feature: 'replacementFeature', + removeOriginalPrivileges: true, + }, + ]; + + const result = addSubFeatureReplacements(mockSubFeature, replacements); + + // Should add empty privileges array for each privilege + expect(result.privilegeGroups[0].privileges[0].replacedBy).toEqual([ + { feature: 'replacementFeature', privileges: [] }, + ]); + }); + + it('adds additional privileges when provided', () => { + const replacements: SubFeatureReplacement[] = [ + { + feature: 'replacementFeature', + removeOriginalPrivileges: false, + additionalPrivileges: { + privilege1: ['extraPriv1', 'extraPriv2'], + privilege2: ['extraPriv3'], + }, + }, + ]; + + const result = addSubFeatureReplacements(mockSubFeature, replacements); + + // Should add additional privileges + expect(result.privilegeGroups[0].privileges[0].replacedBy).toEqual([ + { feature: 'replacementFeature', privileges: ['privilege1', 'extraPriv1', 'extraPriv2'] }, + ]); + expect(result.privilegeGroups[0].privileges[1].replacedBy).toEqual([ + { feature: 'replacementFeature', privileges: ['privilege2', 'extraPriv3'] }, + ]); + }); + + it('appends to existing replacedBy array if present', () => { + const subFeatureWithExistingReplacements: SubFeatureConfig = { + ...mockSubFeature, + privilegeGroups: [ + { + ...mockSubFeature.privilegeGroups[0], + privileges: [ + { + ...mockSubFeature.privilegeGroups[0].privileges[0], + replacedBy: [{ feature: 'existingFeature', privileges: ['existingPrivilege'] }], + }, + ...mockSubFeature.privilegeGroups[0].privileges.slice(1), + ], + }, + ], + }; + + const replacements: SubFeatureReplacement[] = [ + { + feature: 'newFeature', + removeOriginalPrivileges: false, + }, + ]; + + const result = addSubFeatureReplacements(subFeatureWithExistingReplacements, replacements); + + // Should preserve existing replacements and add new ones + expect(result.privilegeGroups[0].privileges[0].replacedBy).toEqual([ + { feature: 'existingFeature', privileges: ['existingPrivilege'] }, + { feature: 'newFeature', privileges: ['privilege1'] }, + ]); + }); + + it('handles multiple replacements', () => { + const replacements: SubFeatureReplacement[] = [ + { + feature: 'feature1', + removeOriginalPrivileges: false, + }, + { + feature: 'feature2', + removeOriginalPrivileges: true, + additionalPrivileges: { + privilege1: ['extra1'], + }, + }, + ]; + + const result = addSubFeatureReplacements(mockSubFeature, replacements); + + // Should add both replacements + expect(result.privilegeGroups[0].privileges[0].replacedBy).toEqual([ + { feature: 'feature1', privileges: ['privilege1'] }, + { feature: 'feature2', privileges: ['extra1'] }, + ]); + }); + }); + + describe('addAllSubFeatureReplacements', () => { + const mockSubFeature1: SubFeatureConfig = { + name: 'SubFeature1', + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + id: 'priv1', + name: 'Privilege 1', + includeIn: 'read', + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + ], + }, + ], + }; + + const mockSubFeature2: SubFeatureConfig = { + name: 'SubFeature2', + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + id: 'priv2', + name: 'Privilege 2', + includeIn: 'all', + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + ], + }, + ], + }; + + it('returns the original map if no replacements are provided', () => { + const subFeaturesMap = new Map([ + ['feature1', mockSubFeature1], + ['feature2', mockSubFeature2], + ]); + + const result = addAllSubFeatureReplacements(subFeaturesMap, []); + + expect(result).toBe(subFeaturesMap); + }); + + it('adds replacements to all subFeatures in the map', () => { + const subFeaturesMap = new Map([ + ['feature1', mockSubFeature1], + ['feature2', mockSubFeature2], + ]); + + const replacements: SubFeatureReplacement[] = [ + { + feature: 'replacementFeature', + removeOriginalPrivileges: false, + }, + ]; + + const result = addAllSubFeatureReplacements(subFeaturesMap, replacements); + + // Should not mutate original + expect( + subFeaturesMap.get('feature1')!.privilegeGroups[0].privileges[0].replacedBy + ).toBeUndefined(); + expect( + subFeaturesMap.get('feature2')!.privilegeGroups[0].privileges[0].replacedBy + ).toBeUndefined(); + + // Should add replacements to all features + expect(result.get('feature1')!.privilegeGroups[0].privileges[0].replacedBy).toEqual([ + { feature: 'replacementFeature', privileges: ['priv1'] }, + ]); + expect(result.get('feature2')!.privilegeGroups[0].privileges[0].replacedBy).toEqual([ + { feature: 'replacementFeature', privileges: ['priv2'] }, + ]); + }); + + it('returns a new map instance and does not mutate the original', () => { + const subFeaturesMap = new Map([ + ['feature1', mockSubFeature1], + ['feature2', mockSubFeature2], + ]); + + const replacements: SubFeatureReplacement[] = [ + { + feature: 'replacementFeature', + removeOriginalPrivileges: false, + }, + ]; + + const result = addAllSubFeatureReplacements(subFeaturesMap, replacements); + + // Should return a new map instance + expect(result).not.toBe(subFeaturesMap); + + // Original map should remain unchanged + expect(subFeaturesMap.get('feature1')).toBe(mockSubFeature1); + expect(subFeaturesMap.get('feature2')).toBe(mockSubFeature2); + }); + }); +}); diff --git a/x-pack/solutions/security/packages/features/src/utils/sub_features.ts b/x-pack/solutions/security/packages/features/src/utils/sub_features.ts new file mode 100644 index 0000000000000..c37064bbe84cd --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/utils/sub_features.ts @@ -0,0 +1,63 @@ +/* + * 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 { cloneDeep } from 'lodash'; +import type { SubFeatureConfig } from '@kbn/features-plugin/common'; +import type { AppSubFeaturesMap, MutableSubFeatureConfig, SubFeatureReplacement } from '../types'; + +/** + * Adds the replacedBy entries to the subFeature's privileges. + * It does not mutate the original subFeature. + * @param subFeature - The subFeature to add replacements to + * @param replacements - The replacements to add + * @returns A new subFeature with the replacements added + */ +export const addSubFeatureReplacements = ( + subFeature: SubFeatureConfig, + replacements: SubFeatureReplacement[] +): SubFeatureConfig => { + if (!replacements.length) { + return subFeature; + } + + const subFeatureWithReplacement = cloneDeep(subFeature) as MutableSubFeatureConfig; + + subFeatureWithReplacement.privilegeGroups.forEach((privilegeGroup) => { + privilegeGroup.privileges.forEach((privilege) => { + privilege.replacedBy ??= []; + for (const replacement of replacements) { + const privileges = !replacement.removeOriginalPrivileges ? [privilege.id] : []; + privileges.push(...(replacement.additionalPrivileges?.[privilege.id] ?? [])); + privilege.replacedBy.push({ feature: replacement.feature, privileges }); + } + }); + }); + + return subFeatureWithReplacement; +}; + +/** + * Adds the replacements to all sub-features in the provided subFeaturesMap. + * It does not mutate the original subFeaturesMap. + * @param subFeaturesMap - The subFeaturesMap to add replacements to + * @param replacements - The replacements to add + * @returns A new subFeaturesMap with the replacements added + */ +export const addAllSubFeatureReplacements = ( + subFeaturesMap: AppSubFeaturesMap, + replacements: SubFeatureReplacement[] +): AppSubFeaturesMap => { + if (!replacements.length) { + return subFeaturesMap; + } + return new Map( + [...subFeaturesMap.entries()].map(([id, subFeature]) => { + const subFeatureWithReplacement = addSubFeatureReplacements(subFeature, replacements); + return [id, subFeatureWithReplacement]; + }) + ); +}; diff --git a/x-pack/solutions/security/packages/features/utils.ts b/x-pack/solutions/security/packages/features/utils.ts new file mode 100644 index 0000000000000..633e8280a9dcb --- /dev/null +++ b/x-pack/solutions/security/packages/features/utils.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/utils'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/cases_privileges.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/cases_product_feature_params.ts similarity index 60% rename from x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/cases_privileges.ts rename to x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/cases_product_feature_params.ts index 9e863385271b9..fc750faed5c60 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/cases_privileges.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/cases_product_feature_params.ts @@ -13,13 +13,14 @@ import { CASES_CONNECTORS_CAPABILITY, GET_CONNECTORS_CONFIGURE_API_TAG, } from '@kbn/cases-plugin/common/constants'; - +import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects'; +import type { CasesFeatureParams } from '@kbn/security-solution-features/src/cases/types'; import { APP_ID } from '../../../common/constants'; const originalCasesUiCapabilities = createCasesUICapabilities(); const originalCasesApiTags = getCasesApiTags(APP_ID); -export const casesUiCapabilities = { +const defaultUiCapabilities = { ...originalCasesUiCapabilities, all: originalCasesUiCapabilities.all.filter( (capability) => capability !== CASES_CONNECTORS_CAPABILITY @@ -29,7 +30,7 @@ export const casesUiCapabilities = { ), }; -export const casesApiTags = { +const defaultApiTags = { ...originalCasesApiTags, all: originalCasesApiTags.all.filter( (capability) => capability !== GET_CONNECTORS_CONFIGURE_API_TAG @@ -38,3 +39,24 @@ export const casesApiTags = { (capability) => capability !== GET_CONNECTORS_CONFIGURE_API_TAG ), }; + +const connectorsUiCapabilities = { + all: [CASES_CONNECTORS_CAPABILITY], + read: [CASES_CONNECTORS_CAPABILITY], +}; +const connectorsApiTags = { + all: [GET_CONNECTORS_CONFIGURE_API_TAG], + read: [GET_CONNECTORS_CONFIGURE_API_TAG], +}; + +export const casesProductFeatureParams: CasesFeatureParams = { + apiTags: { + default: defaultApiTags, + connectors: connectorsApiTags, + }, + uiCapabilities: { + default: defaultUiCapabilities, + connectors: connectorsUiCapabilities, + }, + savedObjects: { files: filesSavedObjectTypes }, +}; 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 e9e03c62eb94d..b72d6562a99f5 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 @@ -12,8 +12,10 @@ import { featuresPluginMock } from '@kbn/features-plugin/server/mocks'; import type { ProductFeatureKeys } from '@kbn/security-solution-features'; import { ALL_PRODUCT_FEATURE_KEYS } from '@kbn/security-solution-features/keys'; +import { coreLifecycleMock } from '@kbn/core-lifecycle-server-mocks'; import { allowedExperimentalValues, type ExperimentalFeatures } from '../../../common'; import { ProductFeaturesService } from './product_features_service'; +import type { SecuritySolutionPluginSetupDependencies } from '../../plugin_contract'; jest.mock('@kbn/security-solution-features/product_features', () => ({ getSecurityFeature: jest.fn(() => ({ @@ -75,142 +77,20 @@ jest.mock('@kbn/security-solution-features/product_features', () => ({ export const createProductFeaturesServiceMock = ( /** What features keys should be enabled. Default is all */ - enabledFeatureKeys: ProductFeatureKeys = [...ALL_PRODUCT_FEATURE_KEYS], + enabledProductFeatureKeys: ProductFeatureKeys = [...ALL_PRODUCT_FEATURE_KEYS], experimentalFeatures: ExperimentalFeatures = { ...allowedExperimentalValues }, featuresPluginSetupContract: FeaturesPluginSetup = featuresPluginMock.createSetup(), logger: Logger = loggingSystemMock.create().get('productFeatureMock') ) => { const productFeaturesService = new ProductFeaturesService(logger, experimentalFeatures); - productFeaturesService.init(featuresPluginSetupContract); + productFeaturesService.setup(coreLifecycleMock.createCoreSetup(), { + features: featuresPluginSetupContract, + } as SecuritySolutionPluginSetupDependencies); - if (enabledFeatureKeys) { + if (enabledProductFeatureKeys) { productFeaturesService.setProductFeaturesConfigurator({ - security: jest.fn().mockReturnValue( - new Map( - enabledFeatureKeys.map((key) => [ - key, - { - privileges: { - all: { - ui: ['entity-analytics'], - api: [`test-entity-analytics`], - }, - read: { - ui: ['entity-analytics'], - api: [`test-entity-analytics`], - }, - }, - }, - ]) - ) - ), - cases: jest.fn().mockReturnValue( - new Map( - enabledFeatureKeys.map((key) => [ - key, - { - privileges: { - all: { - ui: ['entity-analytics'], - api: [`test-entity-analytics`], - }, - read: { - ui: ['entity-analytics'], - api: [`test-entity-analytics`], - }, - }, - }, - ]) - ) - ), - securityAssistant: jest.fn().mockReturnValue( - new Map( - enabledFeatureKeys.map((key) => [ - key, - { - privileges: { - all: { - ui: ['entity-analytics'], - api: [`test-entity-analytics`], - }, - read: { - ui: ['entity-analytics'], - api: [`test-entity-analytics`], - }, - }, - }, - ]) - ) - ), - attackDiscovery: jest.fn().mockReturnValue( - new Map( - enabledFeatureKeys.map((key) => [ - key, - { - privileges: { - all: { - ui: ['entity-analytics'], - api: [`test-entity-analytics`], - }, - read: { - ui: ['entity-analytics'], - api: [`test-entity-analytics`], - }, - }, - }, - ]) - ) - ), - timeline: jest.fn().mockReturnValue( - new Map( - enabledFeatureKeys.map((key) => [ - key, - { - privileges: { - all: { - ui: ['entity-analytics'], - }, - read: { - ui: ['entity-analytics'], - }, - }, - }, - ]) - ) - ), - notes: jest.fn().mockReturnValue( - new Map( - enabledFeatureKeys.map((key) => [ - key, - { - privileges: { - all: { - ui: ['entity-analytics'], - }, - read: { - ui: ['entity-analytics'], - }, - }, - }, - ]) - ) - ), - siemMigrations: jest.fn().mockReturnValue( - new Map( - enabledFeatureKeys.map((key) => [ - key, - { - privileges: { - all: { - api: ['test-api-action'], - ui: ['test-ui-action'], - }, - }, - }, - ]) - ) - ), + enabledProductFeatureKeys, }); } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features.test.ts index 958d952e7ec23..5e3062862c247 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features.test.ts @@ -12,6 +12,8 @@ import type { ProductFeatureKeyType, ProductFeatureKibanaConfig, BaseKibanaFeatureConfig, + ProductFeatureParams, + ProductFeatureGroup, } from '@kbn/security-solution-features'; import type { SubFeatureConfig } from '@kbn/features-plugin/common'; @@ -52,7 +54,6 @@ const baseKibanaFeature: BaseKibanaFeatureConfig = { }; // sub-features definition - const SUB_FEATURE: SubFeatureConfig = { name: 'subFeature1', privilegeGroups: [ @@ -74,6 +75,7 @@ const SUB_FEATURE: SubFeatureConfig = { }, ], }; + const SUB_FEATURE_2: SubFeatureConfig = { name: 'subFeature2', privilegeGroups: [ @@ -144,22 +146,39 @@ const expectedBaseWithTestConfigPrivileges = { }, }; +const testFeatureKey1 = 'test-feature' as ProductFeatureKeyType; +const testFeatureKey2 = 'test-sub-feature' as ProductFeatureKeyType; + +const testFeatureParams: ProductFeatureParams = { + subFeaturesMap, + baseKibanaFeature, + baseKibanaSubFeatureIds: [], +}; + +const logger = loggingSystemMock.create().get('mock'); +const featureGroup = 'test-feature' as ProductFeatureGroup; + +const featuresSetup = { + registerKibanaFeature: jest.fn(), + getKibanaFeatures: jest.fn(), +} as unknown as FeaturesPluginSetup; + describe('ProductFeatures', () => { - describe('setConfig', () => { - it('should register base kibana features', () => { - const featuresSetup = { - registerKibanaFeature: jest.fn(), - getKibanaFeatures: jest.fn(), - } as unknown as FeaturesPluginSetup; - - const productFeatures = new ProductFeatures( - loggingSystemMock.create().get('mock'), - subFeaturesMap, - baseKibanaFeature, - [] - ); + beforeEach(() => { + jest.clearAllMocks(); + // Reset productFeatureConfig for each test + if (testFeatureParams.productFeatureConfig) { + delete testFeatureParams.productFeatureConfig; + } + }); + + describe('register', () => { + it('should register base kibana features with empty enabled keys', () => { + const productFeatures = new ProductFeatures(logger); + + productFeatures.create(featureGroup, [testFeatureParams]); productFeatures.init(featuresSetup); - productFeatures.setConfig(new Map()); + productFeatures.register([], {}); expect(featuresSetup.registerKibanaFeature).toHaveBeenCalledWith({ ...baseKibanaFeature, @@ -168,21 +187,21 @@ describe('ProductFeatures', () => { }); it('should register enabled kibana features', () => { - const featuresSetup = { - registerKibanaFeature: jest.fn(), - getKibanaFeatures: jest.fn(), - } as unknown as FeaturesPluginSetup; - - const productFeatures = new ProductFeatures( - loggingSystemMock.create().get('mock'), - subFeaturesMap, - baseKibanaFeature, - [] - ); + const productFeatures = new ProductFeatures(logger); + + // Setup product feature with static config + const paramsWithConfig = { + ...testFeatureParams, + productFeatureConfig: { + [testFeatureKey1]: testFeaturePrivilegeConfig, + }, + }; + + productFeatures.create(featureGroup, [paramsWithConfig]); productFeatures.init(featuresSetup); - productFeatures.setConfig( - new Map([['test-feature' as ProductFeatureKeyType, testFeaturePrivilegeConfig]]) - ); + + // Register with the specific enabled key + productFeatures.register([testFeatureKey1], {}); expect(featuresSetup.registerKibanaFeature).toHaveBeenCalledWith({ ...baseKibanaFeature, @@ -192,24 +211,22 @@ describe('ProductFeatures', () => { }); it('should register enabled kibana features and sub features', () => { - const featuresSetup = { - registerKibanaFeature: jest.fn(), - getKibanaFeatures: jest.fn(), - } as unknown as FeaturesPluginSetup; - - const productFeatures = new ProductFeatures( - loggingSystemMock.create().get('mock'), - subFeaturesMap, - baseKibanaFeature, - [] - ); + const productFeatures = new ProductFeatures(logger); + + // Setup product feature with static configs for feature and sub-feature + const paramsWithConfig = { + ...testFeatureParams, + productFeatureConfig: { + [testFeatureKey1]: testFeaturePrivilegeConfig, + [testFeatureKey2]: testSubFeaturePrivilegeConfig, + }, + }; + + productFeatures.create(featureGroup, [paramsWithConfig]); productFeatures.init(featuresSetup); - productFeatures.setConfig( - new Map([ - ['test-feature' as ProductFeatureKeyType, testFeaturePrivilegeConfig], - ['test-sub-feature' as ProductFeatureKeyType, testSubFeaturePrivilegeConfig], - ]) - ); + + // Register with both enabled keys + productFeatures.register([testFeatureKey1, testFeatureKey2], {}); expect(featuresSetup.registerKibanaFeature).toHaveBeenCalledWith({ ...baseKibanaFeature, @@ -219,24 +236,23 @@ describe('ProductFeatures', () => { }); it('should register enabled kibana features and default sub features', () => { - const featuresSetup = { - registerKibanaFeature: jest.fn(), - getKibanaFeatures: jest.fn(), - } as unknown as FeaturesPluginSetup; - - const productFeatures = new ProductFeatures( - loggingSystemMock.create().get('mock'), - subFeaturesMap, - baseKibanaFeature, - [SUB_FEATURE_2.name] - ); + const productFeatures = new ProductFeatures(logger); + + // Setup product feature with static configs and default sub-feature + const paramsWithConfig = { + ...testFeatureParams, + baseKibanaSubFeatureIds: [SUB_FEATURE_2.name], + productFeatureConfig: { + [testFeatureKey1]: testFeaturePrivilegeConfig, + [testFeatureKey2]: testSubFeaturePrivilegeConfig, + }, + }; + + productFeatures.create(featureGroup, [paramsWithConfig]); productFeatures.init(featuresSetup); - productFeatures.setConfig( - new Map([ - ['test-feature' as ProductFeatureKeyType, testFeaturePrivilegeConfig], - ['test-sub-feature' as ProductFeatureKeyType, testSubFeaturePrivilegeConfig], - ]) - ); + + // Register with both enabled keys + productFeatures.register([testFeatureKey1, testFeatureKey2], {}); expect(featuresSetup.registerKibanaFeature).toHaveBeenCalledWith({ ...baseKibanaFeature, @@ -244,22 +260,78 @@ describe('ProductFeatures', () => { subFeatures: [SUB_FEATURE, SUB_FEATURE_2], }); }); + + it('should use extensions to modify product feature config', () => { + const productFeatures = new ProductFeatures(logger); + + // Setup base product feature with static config + const paramsWithConfig = { + ...testFeatureParams, + productFeatureConfig: { + [testFeatureKey1]: { + privileges: { + all: { + ui: ['base-action'], + api: ['base-action'], + }, + read: { + ui: ['base-action'], + api: ['base-action'], + }, + }, + }, + }, + }; + + productFeatures.create(featureGroup, [paramsWithConfig]); + productFeatures.init(featuresSetup); + + // Register with extension that adds to the static config + const extensions = { + [featureGroup]: { + allVersions: { + [testFeatureKey1]: { + privileges: { + all: { + ui: ['extension-action'], + api: ['extension-action'], + }, + read: { + ui: ['extension-action'], + api: ['extension-action'], + }, + }, + }, + }, + }, + }; + + productFeatures.register([testFeatureKey1], extensions); + + // Should combine both base and extension actions + expect(featuresSetup.registerKibanaFeature).toHaveBeenCalledWith( + expect.objectContaining({ + privileges: { + all: expect.objectContaining({ + ui: expect.arrayContaining(['base-action', 'extension-action']), + api: expect.arrayContaining(['base-action', 'extension-action']), + }), + read: expect.objectContaining({ + ui: expect.arrayContaining(['base-action', 'extension-action']), + api: expect.arrayContaining(['base-action', 'extension-action']), + }), + }, + }) + ); + }); }); describe('isActionRegistered', () => { it('should register base privilege actions', () => { - const featuresSetup = { - registerKibanaFeature: jest.fn(), - } as unknown as FeaturesPluginSetup; - - const productFeatures = new ProductFeatures( - loggingSystemMock.create().get('mock'), - subFeaturesMap, - baseKibanaFeature, - [] - ); + const productFeatures = new ProductFeatures(logger); + productFeatures.create(featureGroup, [testFeatureParams]); productFeatures.init(featuresSetup); - productFeatures.setConfig(new Map()); + productFeatures.register([], {}); expect(productFeatures.isActionRegistered('api:api-read')).toEqual(true); expect(productFeatures.isActionRegistered('ui:read')).toEqual(true); @@ -274,20 +346,18 @@ describe('ProductFeatures', () => { }); it('should register config privilege actions', () => { - const featuresSetup = { - registerKibanaFeature: jest.fn(), - } as unknown as FeaturesPluginSetup; - - const productFeatures = new ProductFeatures( - loggingSystemMock.create().get('mock'), - subFeaturesMap, - baseKibanaFeature, - [] - ); + const productFeatures = new ProductFeatures(logger); + + const paramsWithConfig = { + ...testFeatureParams, + productFeatureConfig: { + [testFeatureKey1]: testFeaturePrivilegeConfig, + }, + }; + + productFeatures.create(featureGroup, [paramsWithConfig]); productFeatures.init(featuresSetup); - productFeatures.setConfig( - new Map([['test-feature' as ProductFeatureKeyType, testFeaturePrivilegeConfig]]) - ); + productFeatures.register([testFeatureKey1], {}); expect(productFeatures.isActionRegistered('api:api-read')).toEqual(true); expect(productFeatures.isActionRegistered('ui:read')).toEqual(true); @@ -302,23 +372,19 @@ describe('ProductFeatures', () => { }); it('should register config sub-feature privilege actions', () => { - const featuresSetup = { - registerKibanaFeature: jest.fn(), - } as unknown as FeaturesPluginSetup; - - const productFeatures = new ProductFeatures( - loggingSystemMock.create().get('mock'), - subFeaturesMap, - baseKibanaFeature, - [] - ); + const productFeatures = new ProductFeatures(logger); + + const paramsWithConfig = { + ...testFeatureParams, + productFeatureConfig: { + [testFeatureKey1]: testFeaturePrivilegeConfig, + [testFeatureKey2]: testSubFeaturePrivilegeConfig, + }, + }; + + productFeatures.create(featureGroup, [paramsWithConfig]); productFeatures.init(featuresSetup); - productFeatures.setConfig( - new Map([ - ['test-feature' as ProductFeatureKeyType, testFeaturePrivilegeConfig], - ['test-sub-feature' as ProductFeatureKeyType, testSubFeaturePrivilegeConfig], - ]) - ); + productFeatures.register([testFeatureKey1, testFeatureKey2], {}); expect(productFeatures.isActionRegistered('api:api-read')).toEqual(true); expect(productFeatures.isActionRegistered('ui:read')).toEqual(true); @@ -333,23 +399,20 @@ describe('ProductFeatures', () => { }); it('should register default and config sub-feature privilege actions', () => { - const featuresSetup = { - registerKibanaFeature: jest.fn(), - } as unknown as FeaturesPluginSetup; - - const productFeatures = new ProductFeatures( - loggingSystemMock.create().get('mock'), - subFeaturesMap, - baseKibanaFeature, - [SUB_FEATURE_2.name] - ); + const productFeatures = new ProductFeatures(logger); + + const paramsWithConfig = { + ...testFeatureParams, + baseKibanaSubFeatureIds: [SUB_FEATURE_2.name], + productFeatureConfig: { + [testFeatureKey1]: testFeaturePrivilegeConfig, + [testFeatureKey2]: testSubFeaturePrivilegeConfig, + }, + }; + + productFeatures.create(featureGroup, [paramsWithConfig]); productFeatures.init(featuresSetup); - productFeatures.setConfig( - new Map([ - ['test-feature' as ProductFeatureKeyType, testFeaturePrivilegeConfig], - ['test-sub-feature' as ProductFeatureKeyType, testSubFeaturePrivilegeConfig], - ]) - ); + productFeatures.register([testFeatureKey1, testFeatureKey2], {}); expect(productFeatures.isActionRegistered('api:api-read')).toEqual(true); expect(productFeatures.isActionRegistered('ui:read')).toEqual(true); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features.ts index 5433c7572e9b5..8c5f2b7f41ef3 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features.ts @@ -12,47 +12,82 @@ import type { FeaturesPluginSetup, } from '@kbn/features-plugin/server'; import type { - ProductFeaturesConfig, - AppSubFeaturesMap, - BaseKibanaFeatureConfig, + ProductFeatureParams, + ProductFeatureGroup, + ProductFeatureKeyType, + ProductFeaturesConfiguratorExtensions, + ProductFeatureKibanaConfig, } from '@kbn/security-solution-features'; +import { extendProductFeatureConfigs } from '@kbn/security-solution-features/utils'; import { ProductFeaturesConfigMerger } from './product_features_config_merger'; -export class ProductFeatures { - private featureConfigMerger: ProductFeaturesConfigMerger; +export class ProductFeatures { private featuresSetup?: FeaturesPluginSetup; + private readonly groupVersions: Map; private readonly registeredActions: Set; - constructor( - private readonly logger: Logger, - subFeaturesMap: AppSubFeaturesMap, - private readonly baseKibanaFeature: BaseKibanaFeatureConfig, - private readonly baseKibanaSubFeatureIds: T[] - ) { - this.featureConfigMerger = new ProductFeaturesConfigMerger(this.logger, subFeaturesMap); + constructor(private readonly logger: Logger) { + this.groupVersions = new Map(); this.registeredActions = new Set(); } + public create(featureGroup: ProductFeatureGroup, versions: ProductFeatureParams[]) { + this.groupVersions.set(featureGroup, versions); + } + public init(featuresSetup: FeaturesPluginSetup) { this.featuresSetup = featuresSetup; } - public setConfig(productFeatureConfig: ProductFeaturesConfig) { + public register( + enabledProductFeatureKeys: ProductFeatureKeyType[], + extensions: ProductFeaturesConfiguratorExtensions = {} + ) { if (this.featuresSetup == null) { - throw new Error( - 'Cannot sync kibana features as featuresSetup is not present. Did you call init?' - ); + throw new Error('Cannot register product features. Service not initialized.'); } - const completeProductFeatureConfig = this.featureConfigMerger.mergeProductFeatureConfigs( - this.baseKibanaFeature, - this.baseKibanaSubFeatureIds, - Array.from(productFeatureConfig.values()) - ); + const enabledKeys = new Set(enabledProductFeatureKeys); + + for (const [featureGroup, featureGroupVersions] of this.groupVersions.entries()) { + const { allVersions: allVersionsExtensions = {}, version: versionsExtensions = {} } = + extensions[featureGroup] ?? {}; + + for (const featureVersion of featureGroupVersions) { + const versionExtensions = versionsExtensions[featureVersion.baseKibanaFeature.id] ?? {}; - this.logger.debug(() => JSON.stringify(completeProductFeatureConfig)); - this.featuresSetup.registerKibanaFeature(completeProductFeatureConfig); - this.addRegisteredActions(completeProductFeatureConfig); + const extendedConfig = extendProductFeatureConfigs( + featureVersion.productFeatureConfig ?? {}, + allVersionsExtensions, + versionExtensions + ); + + // Filter to include only the configs of enabled keys + const filteredConfig = Object.entries(extendedConfig).reduce( + (acc, [key, value]) => { + if (enabledKeys.has(key as ProductFeatureKeyType)) { + acc.push(value); + } + return acc; + }, + [] + ); + + const featureConfigMerger = new ProductFeaturesConfigMerger( + this.logger, + featureVersion.subFeaturesMap ?? new Map() + ); + + const completeProductFeatureConfig = featureConfigMerger.mergeProductFeatureConfigs( + featureVersion.baseKibanaFeature, + featureVersion.baseKibanaSubFeatureIds ?? [], + filteredConfig + ); + + this.featuresSetup.registerKibanaFeature(completeProductFeatureConfig); + this.addRegisteredActions(completeProductFeatureConfig); + } + } } private addRegisteredActions(config: KibanaFeatureConfig) { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_api_access_control.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_api_access_control.ts new file mode 100644 index 0000000000000..308cb40ad8a17 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_api_access_control.ts @@ -0,0 +1,101 @@ +/* + * 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, RouteAuthz } from '@kbn/core/server'; +import { API_ACTION_PREFIX } from '@kbn/security-solution-features/actions'; +import type { ProductFeatureKeyType } from '@kbn/security-solution-features/keys'; +import type { RecursiveReadonly } from '@kbn/utility-types'; +import type { ProductFeaturesService } from './product_features_service'; + +// The `securitySolutionProductFeature:` prefix is used for ProductFeature based control. +// Should be used only by routes that do not need RBAC, only direct productFeature control. +const APP_FEATURE_TAG_PREFIX = 'securitySolutionProductFeature:'; + +const isAuthzEnabled = (authz?: RecursiveReadonly): authz is AuthzEnabled => { + return Boolean((authz as AuthzEnabled)?.requiredPrivileges); +}; + +/** + * Registers a route access control to ensure that the product features are enabled for the route. + * Specially required for superuser (`*`) roles with universal access to all APIs. + * This control checks two things: + * - `securitySolutionProductFeature:` tag: verifies if the corresponding product feature is enabled. + * - `requiredPrivileges` in the route's authz config: checks if the required privileges are enabled. + */ +export const registerApiAccessControl = ( + service: ProductFeaturesService, + http: HttpServiceSetup +) => { + /** Returns true only if the API privilege is a security action and is disabled */ + const isApiPrivilegeSecurityAndDisabled = (apiPrivilege: string): boolean => { + if (apiPrivilege.startsWith(API_ACTION_PREFIX)) { + return !service.isActionRegistered(`api:${apiPrivilege}`); + } + return false; + }; + + http.registerOnPostAuth((request, response, toolkit) => { + for (const tag of request.route.options.tags ?? []) { + let isEnabled = true; + if (tag.startsWith(APP_FEATURE_TAG_PREFIX)) { + isEnabled = service.isEnabled( + tag.substring(APP_FEATURE_TAG_PREFIX.length) as ProductFeatureKeyType + ); + } + + if (!isEnabled) { + service.logger.warn( + `Accessing disabled route "${request.url.pathname}${request.url.search}": responding with 404` + ); + return response.notFound(); + } + } + + // This control ensures the action privileges have been registered by the productFeature service, + // preventing full access (`*`) roles, such as superuser, from bypassing productFeature controls. + const authz = request.route.options.security?.authz; + if (isAuthzEnabled(authz)) { + const disabled = authz.requiredPrivileges.some((privilegeEntry) => { + if (typeof privilegeEntry === 'object') { + if (privilegeEntry.allRequired) { + if ( + privilegeEntry.allRequired.some((entry) => + typeof entry === 'string' + ? isApiPrivilegeSecurityAndDisabled(entry) + : entry.anyOf.every(isApiPrivilegeSecurityAndDisabled) + ) + ) { + return true; + } + } + if (privilegeEntry.anyRequired) { + if ( + privilegeEntry.anyRequired.every((entry) => + typeof entry === 'string' + ? isApiPrivilegeSecurityAndDisabled(entry) + : entry.allOf.some(isApiPrivilegeSecurityAndDisabled) + ) + ) { + return true; + } + } + return false; + } else { + return isApiPrivilegeSecurityAndDisabled(privilegeEntry); + } + }); + if (disabled) { + service.logger.warn( + `Accessing disabled route "${request.url.pathname}${request.url.search}": responding with 404` + ); + return response.notFound(); + } + } + + return toolkit.next(); + }); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_config_merger.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_config_merger.test.ts index 02ba9ee6e5869..fcbff33063db5 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_config_merger.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_config_merger.test.ts @@ -352,22 +352,22 @@ describe('ProductFeaturesConfigMerger', () => { }); }); - it('should call baseFeatureConfigModifier() for all product features', () => { + it('should call featureConfigModifiers() for all product features', () => { const enabledProductFeaturesConfigs: ProductFeatureKibanaConfig[] = [ { subFeatureIds: ['subFeature3', 'subFeature1'], - baseFeatureConfigModifier: jest - .fn() - .mockImplementation((baseConfig: KibanaFeatureConfig): KibanaFeatureConfig => { - return { ...baseConfig, name: 'NEW NAME' }; + featureConfigModifiers: [ + jest.fn().mockImplementation((baseConfig: KibanaFeatureConfig) => { + baseConfig.name = 'NEW NAME'; }), + ], }, { - baseFeatureConfigModifier: jest - .fn() - .mockImplementation((baseConfig: KibanaFeatureConfig): KibanaFeatureConfig => { - return { ...baseConfig, order: 666 }; + featureConfigModifiers: [ + jest.fn().mockImplementation((baseConfig: KibanaFeatureConfig) => { + baseConfig.order = 666; }), + ], }, ]; @@ -377,23 +377,65 @@ describe('ProductFeaturesConfigMerger', () => { enabledProductFeaturesConfigs ); - expect(enabledProductFeaturesConfigs[0].baseFeatureConfigModifier).toBeCalledWith( - baseKibanaFeature - ); - expect(enabledProductFeaturesConfigs[1].baseFeatureConfigModifier).toBeCalledWith({ + expect(merged).toEqual({ ...baseKibanaFeature, + + // modifications: name: 'NEW NAME', + order: 666, + + subFeatures: [subFeature1, subFeature3], }); + expect(enabledProductFeaturesConfigs[0].featureConfigModifiers![0]).toHaveBeenCalledTimes(1); + expect(enabledProductFeaturesConfigs[1].featureConfigModifiers![0]).toHaveBeenCalledTimes(1); + }); + + it('should call all featureConfigModifiers() for all product features in order', () => { + const enabledProductFeaturesConfigs: ProductFeatureKibanaConfig[] = [ + { + subFeatureIds: ['subFeature3', 'subFeature1'], + featureConfigModifiers: [ + jest.fn().mockImplementation((baseConfig: KibanaFeatureConfig) => { + baseConfig.name = 'NEW NAME'; // overwritten by another featureConfigModifier in the same product feature + }), + jest.fn().mockImplementation((baseConfig: KibanaFeatureConfig) => { + baseConfig.name = 'EVEN NEWER NAME'; + baseConfig.minimumLicense = 'trial'; // overwritten by a second product feature + }), + ], + }, + { + featureConfigModifiers: [ + jest.fn().mockImplementation((baseConfig: KibanaFeatureConfig) => { + baseConfig.order = 666; + }), + jest.fn().mockImplementation((baseConfig: KibanaFeatureConfig) => { + baseConfig.minimumLicense = 'standard'; + }), + ], + }, + ]; + + const merged = merger.mergeProductFeatureConfigs( + baseKibanaFeature, + [], + enabledProductFeaturesConfigs + ); expect(merged).toEqual({ ...baseKibanaFeature, // modifications: - name: 'NEW NAME', + name: 'EVEN NEWER NAME', order: 666, + minimumLicense: 'standard', subFeatures: [subFeature1, subFeature3], }); + expect(enabledProductFeaturesConfigs[0].featureConfigModifiers![0]).toHaveBeenCalledTimes(1); + expect(enabledProductFeaturesConfigs[0].featureConfigModifiers![1]).toHaveBeenCalledTimes(1); + expect(enabledProductFeaturesConfigs[1].featureConfigModifiers![0]).toHaveBeenCalledTimes(1); + expect(enabledProductFeaturesConfigs[1].featureConfigModifiers![1]).toHaveBeenCalledTimes(1); }); it('should merge everything at the same time', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_config_merger.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_config_merger.ts index de8cef06445e3..1f5420578fad0 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_config_merger.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_config_merger.ts @@ -5,14 +5,18 @@ * 2.0. */ -import { cloneDeep, isArray, mergeWith, uniq } from 'lodash'; +import { cloneDeep, mergeWith } from 'lodash'; import type { Logger } from '@kbn/core/server'; import type { KibanaFeatureConfig, SubFeatureConfig } from '@kbn/features-plugin/common'; import type { ProductFeatureKibanaConfig, BaseKibanaFeatureConfig, SubFeaturesPrivileges, + FeatureConfigModifier, + MutableKibanaFeatureConfig, + MutableSubFeatureConfig, } from '@kbn/security-solution-features'; +import { featureConfigMerger } from '@kbn/security-solution-features/utils'; export class ProductFeaturesConfigMerger { constructor( @@ -32,39 +36,42 @@ export class ProductFeaturesConfigMerger { kibanaSubFeatureIds: T[], productFeaturesConfigs: ProductFeatureKibanaConfig[] ): KibanaFeatureConfig { - let mergedKibanaFeatureConfig = cloneDeep(kibanaFeatureConfig) as KibanaFeatureConfig; - const subFeaturesPrivilegesToMerge: SubFeaturesPrivileges[] = []; + const mergedKibanaFeatureConfig = cloneDeep(kibanaFeatureConfig) as MutableKibanaFeatureConfig; + const enabledSubFeaturesIndexed = Object.fromEntries( kibanaSubFeatureIds.map((id) => [id, true]) ); + const subFeaturesPrivilegesToMerge: SubFeaturesPrivileges[] = []; + const allFeatureConfigModifiers: FeatureConfigModifier[] = []; productFeaturesConfigs.forEach((productFeatureConfig) => { const { subFeaturesPrivileges, subFeatureIds, - baseFeatureConfigModifier, + featureConfigModifiers, ...productFeatureConfigToMerge - } = cloneDeep(productFeatureConfig); + } = productFeatureConfig; subFeatureIds?.forEach((subFeatureId) => { enabledSubFeaturesIndexed[subFeatureId] = true; }); - if (baseFeatureConfigModifier) { - mergedKibanaFeatureConfig = baseFeatureConfigModifier(mergedKibanaFeatureConfig); - } - if (subFeaturesPrivileges) { subFeaturesPrivilegesToMerge.push(...subFeaturesPrivileges); } + + if (featureConfigModifiers) { + allFeatureConfigModifiers.push(...featureConfigModifiers); + } + mergeWith(mergedKibanaFeatureConfig, productFeatureConfigToMerge, featureConfigMerger); }); // generate sub features configs from enabled sub feature ids, preserving map order - const mergedKibanaSubFeatures: SubFeatureConfig[] = []; + const mergedKibanaSubFeatures: MutableSubFeatureConfig[] = []; this.subFeaturesMap.forEach((subFeature, id) => { if (enabledSubFeaturesIndexed[id]) { - mergedKibanaSubFeatures.push(cloneDeep(subFeature)); + mergedKibanaSubFeatures.push(cloneDeep(subFeature) as MutableSubFeatureConfig); } }); @@ -75,7 +82,12 @@ export class ProductFeaturesConfigMerger { mergedKibanaFeatureConfig.subFeatures = mergedKibanaSubFeatures; - return mergedKibanaFeatureConfig; + // Apply custom modifications after merging all the product feature configs, including the subFeatures + allFeatureConfigModifiers.forEach((modifier) => { + modifier(mergedKibanaFeatureConfig); + }); + + return Object.freeze(mergedKibanaFeatureConfig) as KibanaFeatureConfig; } /** @@ -107,16 +119,3 @@ export class ProductFeaturesConfigMerger { } } } - -/** - * The customizer used by lodash.mergeWith to merge deep objects - * Uses concatenation for arrays and removes duplicates, objects are merged using lodash.mergeWith default behavior - * */ -function featureConfigMerger(objValue: unknown, srcValue: unknown) { - if (isArray(srcValue)) { - if (isArray(objValue)) { - return uniq(objValue.concat(srcValue)); - } - return srcValue; - } -} 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 b707630e100d4..fecf35431c8c0 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 @@ -8,18 +8,12 @@ import { ProductFeaturesService } from './product_features_service'; 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 { - AssistantSubFeatureId, - CasesSubFeatureId, - SecuritySubFeatureId, -} from '@kbn/security-solution-features/keys'; import { ProductFeatureKey } from '@kbn/security-solution-features/keys'; import { httpServiceMock } from '@kbn/core-http-server-mocks'; import type { @@ -28,6 +22,8 @@ import type { LifecycleResponseFactory, OnPostAuthHandler, } from '@kbn/core-http-server'; +import type { SecuritySolutionPluginSetupDependencies } from '../../plugin_contract'; +import { coreLifecycleMock } from '@kbn/core-lifecycle-server-mocks'; jest.mock('./product_features'); const MockedProductFeatures = ProductFeatures as unknown as jest.MockedClass< @@ -54,6 +50,12 @@ jest.mock('@kbn/security-solution-features/product_features', () => ({ getSiemMigrationsFeature: () => mockGetFeature(), })); +const coreSetup = coreLifecycleMock.createCoreSetup(); +const featuresSetup = featuresPluginMock.createSetup(); +const pluginsSetup = { + features: featuresSetup, +} as unknown as SecuritySolutionPluginSetupDependencies; + describe('ProductFeaturesService', () => { beforeEach(() => { jest.clearAllMocks(); @@ -63,8 +65,8 @@ describe('ProductFeaturesService', () => { const experimentalFeatures = {} as ExperimentalFeatures; new ProductFeaturesService(loggerMock.create(), experimentalFeatures); - expect(mockGetFeature).toHaveBeenCalledTimes(11); - expect(MockedProductFeatures).toHaveBeenCalledTimes(11); + expect(mockGetFeature).toHaveBeenCalled(); + expect(MockedProductFeatures).toHaveBeenCalled(); }); it('should init all ProductFeatures when initialized', () => { @@ -74,137 +76,200 @@ describe('ProductFeaturesService', () => { experimentalFeatures ); - const featuresSetup = featuresPluginMock.createSetup(); - productFeaturesService.init(featuresSetup); - + productFeaturesService.setup(coreSetup, pluginsSetup); expect(MockedProductFeatures.mock.instances[0].init).toHaveBeenCalledWith(featuresSetup); - expect(MockedProductFeatures.mock.instances[1].init).toHaveBeenCalledWith(featuresSetup); - expect(MockedProductFeatures.mock.instances[2].init).toHaveBeenCalledWith(featuresSetup); }); - it('should configure ProductFeatures', () => { - const experimentalFeatures = {} as ExperimentalFeatures; - const productFeaturesService = new ProductFeaturesService( - loggerMock.create(), - experimentalFeatures - ); + describe('setProductFeaturesConfigurator', () => { + it('should configure ProductFeatures with basic configuration', () => { + const experimentalFeatures = {} as ExperimentalFeatures; + const productFeaturesService = new ProductFeaturesService( + loggerMock.create(), + experimentalFeatures + ); - const featuresSetup = featuresPluginMock.createSetup(); - productFeaturesService.init(featuresSetup); - - const mockSecurityConfig = new Map() as ProductFeaturesConfig; - const mockCasesConfig = new Map() as ProductFeaturesConfig; - const mockAssistantConfig = new Map() as ProductFeaturesConfig; - const mockAttackDiscoveryConfig = new Map() as ProductFeaturesConfig; - const mockSiemMigrationsConfig = new Map() as ProductFeaturesConfig; - const mockTimelineConfig = new Map() as ProductFeaturesConfig; - const mockNotesConfig = new Map() as ProductFeaturesConfig; - - const configurator: ProductFeaturesConfigurator = { - security: jest.fn(() => mockSecurityConfig), - cases: jest.fn(() => mockCasesConfig), - securityAssistant: jest.fn(() => mockAssistantConfig), - attackDiscovery: jest.fn(() => mockAttackDiscoveryConfig), - siemMigrations: jest.fn(() => mockSiemMigrationsConfig), - timeline: jest.fn(() => mockTimelineConfig), - notes: jest.fn(() => mockNotesConfig), - }; - productFeaturesService.setProductFeaturesConfigurator(configurator); - - expect(configurator.security).toHaveBeenCalled(); - expect(configurator.cases).toHaveBeenCalled(); - expect(configurator.securityAssistant).toHaveBeenCalled(); - expect(configurator.attackDiscovery).toHaveBeenCalled(); - expect(configurator.siemMigrations).toHaveBeenCalled(); - - expect(MockedProductFeatures.mock.instances[0].setConfig).toHaveBeenCalledWith( - mockSecurityConfig - ); - expect(MockedProductFeatures.mock.instances[1].setConfig).toHaveBeenCalledWith(mockCasesConfig); - expect(MockedProductFeatures.mock.instances[2].setConfig).toHaveBeenCalledWith( - mockAssistantConfig - ); - expect(MockedProductFeatures.mock.instances[3].setConfig).toHaveBeenCalledWith( - mockAttackDiscoveryConfig - ); - expect(MockedProductFeatures.mock.instances[3].setConfig).toHaveBeenCalledWith( - mockSiemMigrationsConfig - ); + productFeaturesService.setup(coreSetup, pluginsSetup); + + // Simple configurator with just enabled keys and no extensions + const configurator: ProductFeaturesConfigurator = { + enabledProductFeatureKeys: [ + ProductFeatureKey.advancedInsights, + ProductFeatureKey.casesConnectors, + ], + extensions: {}, + }; + + productFeaturesService.setProductFeaturesConfigurator(configurator); + + // Verify that register was called with the enabledProductFeatureKeys and empty extensions + const { register } = MockedProductFeatures.mock.instances[0]; + expect(register).toHaveBeenCalledWith( + configurator.enabledProductFeatureKeys, + configurator.extensions + ); + }); + + it('should configure ProductFeatures with extensions', () => { + const log = loggerMock.create(); + const experimentalFeatures = {} as ExperimentalFeatures; + const productFeaturesService = new ProductFeaturesService(log, experimentalFeatures); + + productFeaturesService.setup(coreSetup, pluginsSetup); + + // Configurator with both enabled keys and extensions + const configurator: ProductFeaturesConfigurator = { + enabledProductFeatureKeys: [ + ProductFeatureKey.advancedInsights, + ProductFeatureKey.casesConnectors, + ProductFeatureKey.assistant, + ProductFeatureKey.timeline, + ProductFeatureKey.notes, + ], + extensions: { + security: { + allVersions: { + [ProductFeatureKey.advancedInsights]: { + privileges: { + all: { + api: ['test-api'], + }, + }, + }, + }, + version: {}, + }, + cases: { + allVersions: { + [ProductFeatureKey.casesConnectors]: { + privileges: { + all: { + api: ['test-cases-api'], + }, + }, + }, + }, + version: {}, + }, + }, + }; + + productFeaturesService.setProductFeaturesConfigurator(configurator); + + // Verify that register was called with both enabledProductFeatureKeys and extensions + const { register } = MockedProductFeatures.mock.instances[0]; + expect(register).toHaveBeenCalledWith( + configurator.enabledProductFeatureKeys, + configurator.extensions + ); + + // Verify that the logger was used to log the enabled features + expect(log.get().debug).toHaveBeenCalledWith( + expect.stringContaining('Registering product features:') + ); + }); }); - it('should return isEnabled for enabled features', () => { - const experimentalFeatures = {} as ExperimentalFeatures; - const productFeaturesService = new ProductFeaturesService( - loggerMock.create(), - experimentalFeatures - ); + describe('isEnabled', () => { + it('should throw an error if not configured yet', () => { + const experimentalFeatures = {} as ExperimentalFeatures; + const productFeaturesService = new ProductFeaturesService( + loggerMock.create(), + experimentalFeatures + ); + + productFeaturesService.setup(coreSetup, pluginsSetup); + + // isEnabled should throw error because setProductFeaturesConfigurator hasn't been called + expect(() => { + productFeaturesService.isEnabled(ProductFeatureKey.advancedInsights); + }).toThrow('ProductFeatures has not yet been configured'); + }); + + it('should return true for enabled features and false for disabled features', () => { + const experimentalFeatures = {} as ExperimentalFeatures; + const productFeaturesService = new ProductFeaturesService( + loggerMock.create(), + experimentalFeatures + ); - const featuresSetup = featuresPluginMock.createSetup(); - productFeaturesService.init(featuresSetup); - - const mockSecurityConfig = new Map([ - [ProductFeatureKey.advancedInsights, {}], - [ProductFeatureKey.endpointExceptions, {}], - ]) as ProductFeaturesConfig; - const mockCasesConfig = new Map([ - [ProductFeatureKey.casesConnectors, {}], - ]) as ProductFeaturesConfig; - const mockAssistantConfig = new Map([ - [ProductFeatureKey.assistant, {}], - ]) as ProductFeaturesConfig; - const mockAttackDiscoveryConfig = new Map([ - [ProductFeatureKey.attackDiscovery, {}], - ]) as ProductFeaturesConfig; - const mockSiemMigrationsConfig = new Map([ - [ProductFeatureKey.siemMigrations, {}], - ]) as ProductFeaturesConfig; - const mockTimelineConfig = new Map([[ProductFeatureKey.timeline, {}]]) as ProductFeaturesConfig; - const mockNotesConfig = new Map([[ProductFeatureKey.notes, {}]]) as ProductFeaturesConfig; - - const configurator: ProductFeaturesConfigurator = { - security: jest.fn(() => mockSecurityConfig), - cases: jest.fn(() => mockCasesConfig), - securityAssistant: jest.fn(() => mockAssistantConfig), - attackDiscovery: jest.fn(() => mockAttackDiscoveryConfig), - siemMigrations: jest.fn(() => mockSiemMigrationsConfig), - timeline: jest.fn(() => mockTimelineConfig), - notes: jest.fn(() => mockNotesConfig), - }; - productFeaturesService.setProductFeaturesConfigurator(configurator); - - expect(productFeaturesService.isEnabled(ProductFeatureKey.advancedInsights)).toEqual(true); - expect(productFeaturesService.isEnabled(ProductFeatureKey.endpointExceptions)).toEqual(true); - 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); + productFeaturesService.setup(coreSetup, pluginsSetup); + + const enabledKeys = [ + ProductFeatureKey.advancedInsights, + ProductFeatureKey.endpointExceptions, + ProductFeatureKey.casesConnectors, + ProductFeatureKey.assistant, + ProductFeatureKey.attackDiscovery, + ProductFeatureKey.siemMigrations, + ProductFeatureKey.timeline, + ProductFeatureKey.notes, + ]; + + const configurator: ProductFeaturesConfigurator = { + enabledProductFeatureKeys: enabledKeys, + extensions: {}, + }; + + productFeaturesService.setProductFeaturesConfigurator(configurator); + + expect(productFeaturesService.isEnabled(ProductFeatureKey.advancedInsights)).toEqual(true); + expect(productFeaturesService.isEnabled(ProductFeatureKey.endpointExceptions)).toEqual(true); + 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.timeline)).toEqual(true); + expect(productFeaturesService.isEnabled(ProductFeatureKey.notes)).toEqual(true); + expect(productFeaturesService.isEnabled(ProductFeatureKey.externalRuleActions)).toEqual( + false + ); + }); }); - it('should call isApiPrivilegeEnabled for api actions', () => { - const experimentalFeatures = {} as ExperimentalFeatures; - const productFeaturesService = new ProductFeaturesService( - loggerMock.create(), - experimentalFeatures - ); + describe('getApiActionName', () => { + it('should return API action name with proper prefix', () => { + const experimentalFeatures = {} as ExperimentalFeatures; + const productFeaturesService = new ProductFeaturesService( + loggerMock.create(), + experimentalFeatures + ); - productFeaturesService.isApiPrivilegeEnabled('writeEndpointExceptions'); + expect(productFeaturesService.getApiActionName('test')).toEqual('api:securitySolution-test'); + expect(productFeaturesService.getApiActionName('case/create')).toEqual( + 'api:securitySolution-case/create' + ); + }); + }); - expect(MockedProductFeatures.mock.instances[0].isActionRegistered).toHaveBeenCalledWith( - 'api:securitySolution-writeEndpointExceptions' - ); - expect(MockedProductFeatures.mock.instances[1].isActionRegistered).toHaveBeenCalledWith( - 'api:securitySolution-writeEndpointExceptions' - ); - expect(MockedProductFeatures.mock.instances[2].isActionRegistered).toHaveBeenCalledWith( - 'api:securitySolution-writeEndpointExceptions' - ); + describe('isActionRegistered', () => { + it('should delegate to ProductFeatures.isActionRegistered', () => { + const experimentalFeatures = {} as ExperimentalFeatures; + const productFeaturesService = new ProductFeaturesService( + loggerMock.create(), + experimentalFeatures + ); + + productFeaturesService.setup(coreSetup, pluginsSetup); + + const mockIsActionRegistered = MockedProductFeatures.mock.instances[0] + .isActionRegistered as jest.Mock; + + // Set up mock return values + mockIsActionRegistered.mockReturnValueOnce(true).mockReturnValueOnce(false); + + // Test delegating to isActionRegistered + expect(productFeaturesService.isActionRegistered('action1')).toBe(true); + expect(productFeaturesService.isActionRegistered('action2')).toBe(false); + + // Verify the delegated calls + expect(mockIsActionRegistered).toHaveBeenCalledWith('action1'); + expect(mockIsActionRegistered).toHaveBeenCalledWith('action2'); + }); }); describe('registerApiAccessControl', () => { - const mockHttpSetup = httpServiceMock.createSetupContract(); let lastRegisteredFn: OnPostAuthHandler; - mockHttpSetup.registerOnPostAuth.mockImplementation((fn) => { + coreSetup.http.registerOnPostAuth.mockImplementation((fn) => { lastRegisteredFn = fn; }); @@ -221,9 +286,9 @@ describe('ProductFeaturesService', () => { loggerMock.create(), experimentalFeatures ); - productFeaturesService.registerApiAccessControl(mockHttpSetup); + productFeaturesService.setup(coreSetup, pluginsSetup); - expect(mockHttpSetup.registerOnPostAuth).toHaveBeenCalledTimes(1); + expect(coreSetup.http.registerOnPostAuth).toHaveBeenCalledTimes(1); }); describe('when using productFeature tag', () => { @@ -239,7 +304,7 @@ describe('ProductFeaturesService', () => { loggerMock.create(), experimentalFeatures ); - productFeaturesService.registerApiAccessControl(mockHttpSetup); + productFeaturesService.setup(coreSetup, pluginsSetup); productFeaturesService.isEnabled = jest.fn().mockReturnValueOnce(false); @@ -256,7 +321,7 @@ describe('ProductFeaturesService', () => { loggerMock.create(), experimentalFeatures ); - productFeaturesService.registerApiAccessControl(mockHttpSetup); + productFeaturesService.setup(coreSetup, pluginsSetup); productFeaturesService.isEnabled = jest.fn().mockReturnValueOnce(true); @@ -279,7 +344,7 @@ describe('ProductFeaturesService', () => { loggerMock.create(), experimentalFeatures ); - productFeaturesService.registerApiAccessControl(mockHttpSetup); + productFeaturesService.setup(coreSetup, pluginsSetup); mockIsActionRegistered = MockedProductFeatures.mock.instances[0] .isActionRegistered as jest.Mock; }); 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 9fbfd6d2572de..cab0172d6e02d 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 @@ -5,9 +5,7 @@ * 2.0. */ -import type { AuthzEnabled, HttpServiceSetup, Logger, RouteAuthz } from '@kbn/core/server'; -import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects'; -import type { FeaturesPluginSetup } from '@kbn/features-plugin/server'; +import type { Logger } from '@kbn/core/server'; import type { ProductFeatureKeyType, ProductFeaturesConfigurator, @@ -26,315 +24,88 @@ import { getSiemMigrationsFeature, } from '@kbn/security-solution-features/product_features'; import { API_ACTION_PREFIX } from '@kbn/security-solution-features/actions'; -import type { RecursiveReadonly } from '@kbn/utility-types'; import type { ExperimentalFeatures } from '../../../common'; import { ProductFeatures } from './product_features'; +import { casesProductFeatureParams } from './cases_product_feature_params'; import { securityDefaultSavedObjects, securityNotesSavedObjects, securityTimelineSavedObjects, securityV1SavedObjects, } from './security_saved_objects'; -import { casesApiTags, casesUiCapabilities } from './cases_privileges'; +import { registerApiAccessControl } from './product_features_api_access_control'; +import type { + SecuritySolutionPluginCoreSetupDependencies, + SecuritySolutionPluginSetupDependencies, +} from '../../plugin_contract'; export class ProductFeaturesService { - private securityProductFeatures: ProductFeatures; - private securityV2ProductFeatures: ProductFeatures; - private securityV3ProductFeatures: ProductFeatures; - private casesProductFeatures: ProductFeatures; - private casesProductV2Features: ProductFeatures; - private casesProductFeaturesV3: ProductFeatures; - private securityAssistantProductFeatures: ProductFeatures; - private attackDiscoveryProductFeatures: ProductFeatures; - private timelineProductFeatures: ProductFeatures; - private notesProductFeatures: ProductFeatures; - private siemMigrationsProductFeatures: ProductFeatures; - - private productFeatures?: Set; - - constructor( - private readonly logger: Logger, - private readonly experimentalFeatures: ExperimentalFeatures - ) { - const securityFeature = getSecurityFeature({ - savedObjects: securityV1SavedObjects, - experimentalFeatures: this.experimentalFeatures, - }); - this.securityProductFeatures = new ProductFeatures( - this.logger, - securityFeature.subFeaturesMap, - securityFeature.baseKibanaFeature, - securityFeature.baseKibanaSubFeatureIds - ); - const securityV2Feature = getSecurityV2Feature({ - savedObjects: securityDefaultSavedObjects, - experimentalFeatures: this.experimentalFeatures, - }); - this.securityV2ProductFeatures = new ProductFeatures( - this.logger, - securityV2Feature.subFeaturesMap, - securityV2Feature.baseKibanaFeature, - securityV2Feature.baseKibanaSubFeatureIds - ); - - const securityV3Feature = getSecurityV3Feature({ - savedObjects: securityDefaultSavedObjects, - experimentalFeatures: this.experimentalFeatures, - }); - this.securityV3ProductFeatures = new ProductFeatures( - this.logger, - securityV3Feature.subFeaturesMap, - securityV3Feature.baseKibanaFeature, - securityV3Feature.baseKibanaSubFeatureIds - ); - - const casesFeature = getCasesFeature({ - uiCapabilities: casesUiCapabilities, - apiTags: casesApiTags, - savedObjects: { files: filesSavedObjectTypes }, - }); - - this.casesProductFeatures = new ProductFeatures( - this.logger, - casesFeature.subFeaturesMap, - casesFeature.baseKibanaFeature, - casesFeature.baseKibanaSubFeatureIds - ); - - const casesV2Feature = getCasesV2Feature({ - uiCapabilities: casesUiCapabilities, - apiTags: casesApiTags, - savedObjects: { files: filesSavedObjectTypes }, - }); - - this.casesProductV2Features = new ProductFeatures( - this.logger, - casesV2Feature.subFeaturesMap, - casesV2Feature.baseKibanaFeature, - casesV2Feature.baseKibanaSubFeatureIds - ); - - const casesV3Feature = getCasesV3Feature({ - uiCapabilities: casesUiCapabilities, - apiTags: casesApiTags, - savedObjects: { files: filesSavedObjectTypes }, - }); - this.casesProductFeaturesV3 = new ProductFeatures( - this.logger, - casesV3Feature.subFeaturesMap, - casesV3Feature.baseKibanaFeature, - casesV3Feature.baseKibanaSubFeatureIds - ); - - const assistantFeature = getAssistantFeature(this.experimentalFeatures); - this.securityAssistantProductFeatures = new ProductFeatures( - this.logger, - assistantFeature.subFeaturesMap, - assistantFeature.baseKibanaFeature, - assistantFeature.baseKibanaSubFeatureIds - ); - - const attackDiscoveryFeature = getAttackDiscoveryFeature(); - - this.attackDiscoveryProductFeatures = new ProductFeatures( - this.logger, - attackDiscoveryFeature.subFeaturesMap, - attackDiscoveryFeature.baseKibanaFeature, - attackDiscoveryFeature.baseKibanaSubFeatureIds - ); - - const timelineFeature = getTimelineFeature({ - savedObjects: securityTimelineSavedObjects, - experimentalFeatures: {}, - }); - this.timelineProductFeatures = new ProductFeatures( - this.logger, - timelineFeature.subFeaturesMap, - timelineFeature.baseKibanaFeature, - timelineFeature.baseKibanaSubFeatureIds - ); - - const notesFeature = getNotesFeature({ - savedObjects: securityNotesSavedObjects, - experimentalFeatures: {}, - }); - this.notesProductFeatures = new ProductFeatures( - this.logger, - notesFeature.subFeaturesMap, - notesFeature.baseKibanaFeature, - notesFeature.baseKibanaSubFeatureIds - ); - - const siemMigrationsFeature = getSiemMigrationsFeature(); - this.siemMigrationsProductFeatures = new ProductFeatures( - this.logger, - siemMigrationsFeature.subFeaturesMap, - siemMigrationsFeature.baseKibanaFeature, - siemMigrationsFeature.baseKibanaSubFeatureIds - ); + public readonly logger: Logger; + private productFeaturesRegistry: ProductFeatures; + private enabledProductFeatures?: Set; + + constructor(loggerFactory: Logger, experimentalFeatures: ExperimentalFeatures) { + this.logger = loggerFactory.get('productFeaturesService'); + this.productFeaturesRegistry = new ProductFeatures(this.logger); + + const securityFeatureParams = { experimentalFeatures }; + this.productFeaturesRegistry.create('security', [ + getSecurityFeature({ ...securityFeatureParams, savedObjects: securityV1SavedObjects }), + getSecurityV2Feature({ ...securityFeatureParams, savedObjects: securityDefaultSavedObjects }), + getSecurityV3Feature({ ...securityFeatureParams, savedObjects: securityDefaultSavedObjects }), + ]); + this.productFeaturesRegistry.create('cases', [ + getCasesFeature(casesProductFeatureParams), + getCasesV2Feature(casesProductFeatureParams), + getCasesV3Feature(casesProductFeatureParams), + ]); + this.productFeaturesRegistry.create('securityAssistant', [ + getAssistantFeature(experimentalFeatures), + ]); + this.productFeaturesRegistry.create('attackDiscovery', [getAttackDiscoveryFeature()]); + this.productFeaturesRegistry.create('timeline', [ + getTimelineFeature({ ...securityFeatureParams, savedObjects: securityTimelineSavedObjects }), + ]); + this.productFeaturesRegistry.create('notes', [ + getNotesFeature({ ...securityFeatureParams, savedObjects: securityNotesSavedObjects }), + ]); + if (!experimentalFeatures.siemMigrationsDisabled) { + this.productFeaturesRegistry.create('siemMigrations', [getSiemMigrationsFeature()]); + } } - public init(featuresSetup: FeaturesPluginSetup) { - this.securityProductFeatures.init(featuresSetup); - this.securityV2ProductFeatures.init(featuresSetup); - this.securityV3ProductFeatures.init(featuresSetup); - this.casesProductFeatures.init(featuresSetup); - this.casesProductV2Features.init(featuresSetup); - this.casesProductFeaturesV3.init(featuresSetup); - this.securityAssistantProductFeatures.init(featuresSetup); - this.attackDiscoveryProductFeatures.init(featuresSetup); - this.timelineProductFeatures.init(featuresSetup); - this.notesProductFeatures.init(featuresSetup); - this.siemMigrationsProductFeatures.init(featuresSetup); + /** Initializes the features plugin setup */ + public setup( + core: SecuritySolutionPluginCoreSetupDependencies, + plugins: SecuritySolutionPluginSetupDependencies + ) { + this.productFeaturesRegistry.init(plugins.features); + registerApiAccessControl(this, core.http); } + /** Merges configurations of all the product features and registers them as Kibana features */ public setProductFeaturesConfigurator(configurator: ProductFeaturesConfigurator) { - const securityProductFeaturesConfig = configurator.security(); - this.securityProductFeatures.setConfig(securityProductFeaturesConfig); - this.securityV2ProductFeatures.setConfig(securityProductFeaturesConfig); - this.securityV3ProductFeatures.setConfig(securityProductFeaturesConfig); - - const casesProductFeaturesConfig = configurator.cases(); - this.casesProductFeatures.setConfig(casesProductFeaturesConfig); - this.casesProductV2Features.setConfig(casesProductFeaturesConfig); - this.casesProductFeaturesV3.setConfig(casesProductFeaturesConfig); - - const securityAssistantProductFeaturesConfig = configurator.securityAssistant(); - this.securityAssistantProductFeatures.setConfig(securityAssistantProductFeaturesConfig); - - const attackDiscoveryProductFeaturesConfig = configurator.attackDiscovery(); - this.attackDiscoveryProductFeatures.setConfig(attackDiscoveryProductFeaturesConfig); - - const timelineProductFeaturesConfig = configurator.timeline(); - this.timelineProductFeatures.setConfig(timelineProductFeaturesConfig); + const { enabledProductFeatureKeys, extensions } = configurator; + this.logger.debug(`Registering product features: ${enabledProductFeatureKeys.join(', ')}`); - const notesProductFeaturesConfig = configurator.notes(); - this.notesProductFeatures.setConfig(notesProductFeaturesConfig); + this.productFeaturesRegistry.register(enabledProductFeatureKeys, extensions); - let siemMigrationsProductFeaturesConfig = new Map(); - if (!this.experimentalFeatures.siemMigrationsDisabled) { - siemMigrationsProductFeaturesConfig = configurator.siemMigrations(); - this.siemMigrationsProductFeatures.setConfig(siemMigrationsProductFeaturesConfig); - } - - this.productFeatures = new Set( - Object.freeze([ - ...securityProductFeaturesConfig.keys(), - ...casesProductFeaturesConfig.keys(), - ...securityAssistantProductFeaturesConfig.keys(), - ...attackDiscoveryProductFeaturesConfig.keys(), - ...timelineProductFeaturesConfig.keys(), - ...notesProductFeaturesConfig.keys(), - ...siemMigrationsProductFeaturesConfig.keys(), - ]) as readonly ProductFeatureKeyType[] - ); + this.enabledProductFeatures = new Set(enabledProductFeatureKeys); } + /** Function to check if a specific product feature key is enabled */ public isEnabled(productFeatureKey: ProductFeatureKeyType): boolean { - if (!this.productFeatures) { + if (!this.enabledProductFeatures) { throw new Error('ProductFeatures has not yet been configured'); } - return this.productFeatures.has(productFeatureKey); + return this.enabledProductFeatures.has(productFeatureKey); } + /** Function to check if a specific privilege action has been registered in the Kibana features */ public isActionRegistered(action: string) { - return ( - this.securityProductFeatures.isActionRegistered(action) || - this.securityV2ProductFeatures.isActionRegistered(action) || - this.securityV3ProductFeatures.isActionRegistered(action) || - this.casesProductFeatures.isActionRegistered(action) || - this.casesProductV2Features.isActionRegistered(action) || - this.securityAssistantProductFeatures.isActionRegistered(action) || - this.attackDiscoveryProductFeatures.isActionRegistered(action) || - this.timelineProductFeatures.isActionRegistered(action) || - this.notesProductFeatures.isActionRegistered(action) || - this.siemMigrationsProductFeatures.isActionRegistered(action) - ); + return this.productFeaturesRegistry.isActionRegistered(action); } + /** Function to get the correct API action name for a specific api privilege */ public getApiActionName = (apiPrivilege: string) => `api:${API_ACTION_PREFIX}${apiPrivilege}`; - - /** @deprecated Use security.authz.requiredPrivileges instead */ - public isApiPrivilegeEnabled(apiPrivilege: string) { - return this.isActionRegistered(this.getApiActionName(apiPrivilege)); - } - - public registerApiAccessControl(http: HttpServiceSetup) { - // The `securitySolutionProductFeature:` prefix is used for ProductFeature based control. - // Should be used only by routes that do not need RBAC, only direct productFeature control. - const APP_FEATURE_TAG_PREFIX = 'securitySolutionProductFeature:'; - - const isAuthzEnabled = (authz?: RecursiveReadonly): authz is AuthzEnabled => { - return Boolean((authz as AuthzEnabled)?.requiredPrivileges); - }; - - /** Returns true only if the API privilege is a security action and is disabled */ - const isApiPrivilegeSecurityAndDisabled = (apiPrivilege: string): boolean => { - if (apiPrivilege.startsWith(API_ACTION_PREFIX)) { - return !this.isActionRegistered(`api:${apiPrivilege}`); - } - return false; - }; - - http.registerOnPostAuth((request, response, toolkit) => { - for (const tag of request.route.options.tags ?? []) { - let isEnabled = true; - if (tag.startsWith(APP_FEATURE_TAG_PREFIX)) { - isEnabled = this.isEnabled( - tag.substring(APP_FEATURE_TAG_PREFIX.length) as ProductFeatureKeyType - ); - } - - if (!isEnabled) { - this.logger.warn( - `Accessing disabled route "${request.url.pathname}${request.url.search}": responding with 404` - ); - return response.notFound(); - } - } - - // This control ensures the action privileges have been registered by the productFeature service, - // preventing full access (`*`) roles, such as superuser, from bypassing productFeature controls. - const authz = request.route.options.security?.authz; - if (isAuthzEnabled(authz)) { - const disabled = authz.requiredPrivileges.some((privilegeEntry) => { - if (typeof privilegeEntry === 'object') { - if (privilegeEntry.allRequired) { - if ( - privilegeEntry.allRequired.some((entry) => - typeof entry === 'string' - ? isApiPrivilegeSecurityAndDisabled(entry) - : entry.anyOf.every(isApiPrivilegeSecurityAndDisabled) - ) - ) { - return true; - } - } - if (privilegeEntry.anyRequired) { - if ( - privilegeEntry.anyRequired.every((entry) => - typeof entry === 'string' - ? isApiPrivilegeSecurityAndDisabled(entry) - : entry.allOf.some(isApiPrivilegeSecurityAndDisabled) - ) - ) { - return true; - } - } - return false; - } else { - return isApiPrivilegeSecurityAndDisabled(privilegeEntry); - } - }); - if (disabled) { - this.logger.warn( - `Accessing disabled route "${request.url.pathname}${request.url.search}": responding with 404` - ); - return response.notFound(); - } - } - - return toolkit.next(); - }); - } } diff --git a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts index de3be30a08dc1..0f57878dba494 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts @@ -233,7 +233,7 @@ export class Plugin implements ISecuritySolutionPlugin { }); initUiSettings(core.uiSettings, experimentalFeatures, config.enableUiSettingsValidations); - productFeaturesService.init(plugins.features); + productFeaturesService.setup(core, plugins); events.forEach((eventConfig) => { core.analytics.registerEventType(eventConfig); @@ -310,7 +310,6 @@ export class Plugin implements ISecuritySolutionPlugin { productFeaturesService, }); - productFeaturesService.registerApiAccessControl(core.http); const router = core.http.createRouter(); core.http.registerRouteHandlerContext( APP_ID, diff --git a/x-pack/solutions/security/plugins/security_solution/tsconfig.json b/x-pack/solutions/security/plugins/security_solution/tsconfig.json index 9a3d6b7945193..dfe792e0a3a2a 100644 --- a/x-pack/solutions/security/plugins/security_solution/tsconfig.json +++ b/x-pack/solutions/security/plugins/security_solution/tsconfig.json @@ -15,7 +15,8 @@ "scripts/**/*.json", "public/**/*.json", "../../../../../typings/**/*", - "emotion.d.ts" ], + "emotion.d.ts" + ], "exclude": [ "target/**/*", "**/cypress/**", @@ -254,6 +255,7 @@ "@kbn/elastic-assistant-shared-state", "@kbn/elastic-assistant-shared-state-plugin", "@kbn/spaces-utils", + "@kbn/core-lifecycle-server-mocks", "@kbn/licensing-types", "@kbn/core-metrics-server", ] diff --git a/x-pack/solutions/security/plugins/security_solution_ess/server/jest.config.js b/x-pack/solutions/security/plugins/security_solution_ess/server/jest.config.js new file mode 100644 index 0000000000000..e00fc9f56e033 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution_ess/server/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** @type {import('@jest/types').Config.InitialOptions} */ +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../..', + roots: ['/x-pack/solutions/security/plugins/security_solution_ess/server/'], +}; diff --git a/x-pack/solutions/security/plugins/security_solution_ess/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution_ess/server/plugin.ts index 6c0edcf40dbaf..fada6669bc444 100644 --- a/x-pack/solutions/security/plugins/security_solution_ess/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution_ess/server/plugin.ts @@ -6,7 +6,7 @@ */ import type { Plugin, CoreSetup } from '@kbn/core/server'; -import { getProductProductFeaturesConfigurator } from './product_features'; +import { registerProductFeatures } from './product_features'; import { DEFAULT_PRODUCT_FEATURES } from '../common/constants'; import type { @@ -26,9 +26,7 @@ export class SecuritySolutionEssPlugin > { public setup(_coreSetup: CoreSetup, pluginsSetup: SecuritySolutionEssPluginSetupDeps) { - const productFeaturesConfigurator = - getProductProductFeaturesConfigurator(DEFAULT_PRODUCT_FEATURES); - pluginsSetup.securitySolution.setProductFeaturesConfigurator(productFeaturesConfigurator); + registerProductFeatures(pluginsSetup, DEFAULT_PRODUCT_FEATURES); return {}; } diff --git a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/assistant_product_features_config.ts b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/assistant_product_features_config.ts deleted file mode 100644 index 732128415a394..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/assistant_product_features_config.ts +++ /dev/null @@ -1,44 +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 { - ProductFeatureKeys, - ProductFeatureKibanaConfig, - ProductFeaturesAssistantConfig, -} from '@kbn/security-solution-features'; -import { - assistantDefaultProductFeaturesConfig, - createEnabledProductFeaturesConfigMap, -} from '@kbn/security-solution-features/config'; -import type { - ProductFeatureAssistantKey, - AssistantSubFeatureId, -} from '@kbn/security-solution-features/keys'; - -export const getSecurityAssistantProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesAssistantConfig => { - return createEnabledProductFeaturesConfigMap( - assistantProductFeaturesConfig, - enabledProductFeatureKeys - ); - }; - -/** - * Maps the ProductFeatures keys to Kibana privileges that will be merged - * into the base privileges config for the Security Assistant app. - * - * Privileges can be added in different ways: - * - `privileges`: the privileges that will be added directly into the main Security Assistant feature. - * - `subFeatureIds`: the ids of the sub-features that will be added into the Assistant subFeatures entry. - * - `subFeaturesPrivileges`: the privileges that will be added into the existing Assistant subFeature with the privilege `id` specified. - */ -const assistantProductFeaturesConfig: Record< - ProductFeatureAssistantKey, - ProductFeatureKibanaConfig -> = { - ...assistantDefaultProductFeaturesConfig, - // ess-specific app features configs here -}; diff --git a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/attack_discovery_product_features_config.ts b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/attack_discovery_product_features_config.ts deleted file mode 100644 index 9e575e805e203..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/attack_discovery_product_features_config.ts +++ /dev/null @@ -1,41 +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 { - ProductFeatureKeys, - ProductFeatureKibanaConfig, - ProductFeaturesAttackDiscoveryConfig, -} from '@kbn/security-solution-features'; -import { - attackDiscoveryDefaultProductFeaturesConfig, - createEnabledProductFeaturesConfigMap, -} from '@kbn/security-solution-features/config'; -import type { ProductFeatureAttackDiscoveryKey } 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 attackDiscoveryProductFeaturesConfig: Record< - ProductFeatureAttackDiscoveryKey, - ProductFeatureKibanaConfig -> = { - ...attackDiscoveryDefaultProductFeaturesConfig, - // ess-specific app features configs here -}; - -export const getAttackDiscoveryProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesAttackDiscoveryConfig => - createEnabledProductFeaturesConfigMap( - attackDiscoveryProductFeaturesConfig, - enabledProductFeatureKeys - ); diff --git a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/cases_product_features_config.ts b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/cases_product_features_config.ts deleted file mode 100644 index cdf8d10742373..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/cases_product_features_config.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import type { - ProductFeatureKibanaConfig, - ProductFeaturesCasesConfig, - ProductFeatureKeys, -} from '@kbn/security-solution-features'; -import type { - ProductFeatureCasesKey, - CasesSubFeatureId, -} from '@kbn/security-solution-features/keys'; -import { - getCasesDefaultProductFeaturesConfig, - createEnabledProductFeaturesConfigMap, -} from '@kbn/security-solution-features/config'; - -import { - CASES_CONNECTORS_CAPABILITY, - GET_CONNECTORS_CONFIGURE_API_TAG, -} from '@kbn/cases-plugin/common/constants'; - -export const getCasesProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesCasesConfig => { - return createEnabledProductFeaturesConfigMap( - casesProductFeaturesConfig, - enabledProductFeatureKeys - ); - }; - -/** - * Maps the ProductFeatures keys to Kibana privileges that will be merged - * into the base privileges config for the Security Cases app. - * - * Privileges can be added in different ways: - * - `privileges`: the privileges that will be added directly into the main Security Cases feature. - * - `subFeatureIds`: the ids of the sub-features that will be added into the Cases subFeatures entry. - * - `subFeaturesPrivileges`: the privileges that will be added into the existing Cases subFeature with the privilege `id` specified. - */ -const casesProductFeaturesConfig: Record< - ProductFeatureCasesKey, - ProductFeatureKibanaConfig -> = { - ...getCasesDefaultProductFeaturesConfig({ - apiTags: { connectors: GET_CONNECTORS_CONFIGURE_API_TAG }, - uiCapabilities: { connectors: CASES_CONNECTORS_CAPABILITY }, - }), - // ess-specific app features configs here -}; 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 8370863321226..fcb60b4df64a5 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,28 +5,16 @@ * 2.0. */ -import type { - ProductFeatureKeys, - ProductFeaturesConfigurator, -} from '@kbn/security-solution-features'; -import { getCasesProductFeaturesConfigurator } from './cases_product_features_config'; -import { getSecurityProductFeaturesConfigurator } from './security_product_features_config'; -import { getSecurityAssistantProductFeaturesConfigurator } from './assistant_product_features_config'; -import { getAttackDiscoveryProductFeaturesConfigurator } from './attack_discovery_product_features_config'; -import { getTimelineProductFeaturesConfigurator } from './timeline_product_features_config'; -import { getNotesProductFeaturesConfigurator } from './notes_product_features_config'; -import { getSiemMigrationsProductFeaturesConfigurator } from './siem_migrations_product_features_config'; +import type { ProductFeatureKeys } from '@kbn/security-solution-features'; +import type { SecuritySolutionEssPluginSetupDeps } from '../types'; +import { productFeaturesExtensions } from './product_features_extensions'; -export const getProductProductFeaturesConfigurator = ( +export const registerProductFeatures = ( + pluginsSetup: SecuritySolutionEssPluginSetupDeps, enabledProductFeatureKeys: ProductFeatureKeys -): ProductFeaturesConfigurator => { - return { - security: getSecurityProductFeaturesConfigurator(enabledProductFeatureKeys), - cases: getCasesProductFeaturesConfigurator(enabledProductFeatureKeys), - securityAssistant: getSecurityAssistantProductFeaturesConfigurator(enabledProductFeatureKeys), - attackDiscovery: getAttackDiscoveryProductFeaturesConfigurator(enabledProductFeatureKeys), - timeline: getTimelineProductFeaturesConfigurator(enabledProductFeatureKeys), - notes: getNotesProductFeaturesConfigurator(enabledProductFeatureKeys), - siemMigrations: getSiemMigrationsProductFeaturesConfigurator(enabledProductFeatureKeys), - }; +): void => { + pluginsSetup.securitySolution.setProductFeaturesConfigurator({ + enabledProductFeatureKeys, + extensions: productFeaturesExtensions, + }); }; diff --git a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/notes_product_features_config.ts b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/notes_product_features_config.ts deleted file mode 100644 index 0b9f1e143044a..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/notes_product_features_config.ts +++ /dev/null @@ -1,37 +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 { - ProductFeatureKeys, - ProductFeatureKibanaConfig, - ProductFeaturesNotesConfig, -} from '@kbn/security-solution-features'; -import { - notesDefaultProductFeaturesConfig, - createEnabledProductFeaturesConfigMap, -} from '@kbn/security-solution-features/config'; -import type { ProductFeatureNotesFeatureKey } 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 notesProductFeaturesConfig: Record< - ProductFeatureNotesFeatureKey, - ProductFeatureKibanaConfig -> = { - ...notesDefaultProductFeaturesConfig, - // ess-specific app features configs here -}; - -export const getNotesProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesNotesConfig => - createEnabledProductFeaturesConfigMap(notesProductFeaturesConfig, enabledProductFeatureKeys); diff --git a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/product_features_extensions.test.ts b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/product_features_extensions.test.ts new file mode 100644 index 0000000000000..ae0a7ced79d7f --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/product_features_extensions.test.ts @@ -0,0 +1,116 @@ +/* + * 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 { updateGlobalArtifactManageReplacements } from './product_features_extensions'; +import { SECURITY_FEATURE_ID_V3 } from '@kbn/security-solution-features/constants'; +import type { MutableKibanaFeatureConfig } from '@kbn/security-solution-features'; +import { cloneDeep } from 'lodash'; + +const baseFeatureConfig: MutableKibanaFeatureConfig = { + id: 'siem', + name: 'Security Feature', + app: ['securitySolution'], + category: { id: 'security', label: 'Security' }, + privileges: { + all: { + savedObject: { + all: ['*'], + read: ['*'], + }, + ui: ['all'], + api: [`${SECURITY_FEATURE_ID_V3}-all`], + }, + read: { + savedObject: { + all: ['*'], + read: ['*'], + }, + ui: ['read'], + api: [`${SECURITY_FEATURE_ID_V3}-read`], + }, + }, +}; + +describe('updateGlobalArtifactManageReplacements', () => { + let featureConfig: MutableKibanaFeatureConfig; + + beforeEach(() => { + featureConfig = cloneDeep(baseFeatureConfig); + }); + + it('should do nothing if replacedBy is not present', () => { + const originalConfig = JSON.parse(JSON.stringify(featureConfig)); + + updateGlobalArtifactManageReplacements(featureConfig as MutableKibanaFeatureConfig); + + expect(featureConfig).toEqual(originalConfig); + }); + + it('should modify privileges for SECURITY_FEATURE_ID_V3 in both default and minimal', () => { + const testFeatureConfig = { + ...featureConfig, + privileges: { + ...featureConfig.privileges, + all: { + ...featureConfig.privileges?.all, + replacedBy: { + default: [ + { feature: SECURITY_FEATURE_ID_V3, privileges: ['all'] }, + { feature: 'other_feature', privileges: ['all'] }, + ], + minimal: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['all'] }], + }, + }, + }, + }; + + updateGlobalArtifactManageReplacements(testFeatureConfig as MutableKibanaFeatureConfig); + + const replacedBy = testFeatureConfig.privileges.all.replacedBy; + + // Default privileges modified + const v3Default = replacedBy.default.find( + ({ feature }: { feature: string }) => feature === SECURITY_FEATURE_ID_V3 + ); + expect(v3Default?.privileges).toEqual(['minimal_all', 'global_artifact_management_all']); + + // Minimal privileges modified + const v3Minimal = replacedBy.minimal.find( + ({ feature }: { feature: string }) => feature === SECURITY_FEATURE_ID_V3 + ); + expect(v3Minimal?.privileges).toEqual(['minimal_all', 'global_artifact_management_all']); + + // Ensure other features remain unchanged + const otherFeature = replacedBy.default.find( + ({ feature }: { feature: string }) => feature === 'other_feature' + ); + expect(otherFeature?.privileges).toEqual(['all']); + }); + + it('should only modify existing SECURITY_FEATURE_ID_V3 entries', () => { + const testFeatureConfig = { + ...featureConfig, + privileges: { + ...featureConfig.privileges, + all: { + ...featureConfig.privileges?.all, + replacedBy: { + default: [{ feature: 'other_feature', privileges: ['all'] }], + minimal: [{ feature: 'other_feature', privileges: ['all'] }], + }, + }, + }, + }; + + updateGlobalArtifactManageReplacements(testFeatureConfig as MutableKibanaFeatureConfig); + + const replacedBy = testFeatureConfig.privileges.all.replacedBy; + + // No SECURITY_FEATURE_ID_V3, so no changes + expect(replacedBy.default[0].privileges).toEqual(['all']); + expect(replacedBy.minimal[0].privileges).toEqual(['all']); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/product_features_extensions.ts b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/product_features_extensions.ts new file mode 100644 index 0000000000000..8e177b0a5b6a9 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/product_features_extensions.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 { SECURITY_FEATURE_ID_V3 } from '@kbn/security-solution-features/constants'; +import { APP_ID } from '@kbn/security-solution-plugin/common'; +import { ProductFeatureSecurityKey } from '@kbn/security-solution-features/keys'; +import type { + MutableKibanaFeatureConfig, + ProductFeaturesConfiguratorExtensions, +} from '@kbn/security-solution-features'; + +export const productFeaturesExtensions: ProductFeaturesConfiguratorExtensions = { + security: { + allVersions: { + [ProductFeatureSecurityKey.endpointExceptions]: { + privileges: { + all: { + ui: ['showEndpointExceptions', 'crudEndpointExceptions'], + api: [`${APP_ID}-showEndpointExceptions`, `${APP_ID}-crudEndpointExceptions`], + }, + read: { + ui: ['showEndpointExceptions'], + api: [`${APP_ID}-showEndpointExceptions`], + }, + }, + }, + }, + version: { + siem: { + [ProductFeatureSecurityKey.endpointArtifactManagement]: { + featureConfigModifiers: [updateGlobalArtifactManageReplacements], + }, + }, + siemV2: { + [ProductFeatureSecurityKey.endpointArtifactManagement]: { + featureConfigModifiers: [updateGlobalArtifactManageReplacements], + }, + }, + }, + }, +}; + +// When endpointArtifactManagement PLI is enabled, the replacedBy to the siemV3 feature needs to +// account for the privileges of the additional sub-features that it introduces, migrating them correctly. +// This needs to be done here because the replacements of serverless and ESS are different. +export function updateGlobalArtifactManageReplacements( + featureConfig: MutableKibanaFeatureConfig +): void { + const replacedBy = featureConfig.privileges?.all?.replacedBy; + if (!replacedBy) { + return; + } + + if ('default' in replacedBy) { + const v3Default = replacedBy.default.find(({ feature }) => feature === SECURITY_FEATURE_ID_V3); + if (v3Default) { + // Override replaced privileges from `all` to `minimal_all` with additional sub-features privileges + v3Default.privileges = [ + 'minimal_all', + 'global_artifact_management_all', // Enabling sub-features toggle to show that Global Artifact Management is now provided to the user. + ]; + } + } + + if ('minimal' in replacedBy) { + const v3Minimal = replacedBy.minimal.find(({ feature }) => feature === SECURITY_FEATURE_ID_V3); + if (v3Minimal) { + // Override replaced privileges from `all` to `minimal_all` with additional sub-features privileges + v3Minimal.privileges = [ + 'minimal_all', + 'global_artifact_management_all', // on ESS, Endpoint Exception ALL is included in siem:MINIMAL_ALL + ]; + } + } +} diff --git a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/security_product_features_config.ts b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/security_product_features_config.ts deleted file mode 100644 index 955152b1c84fc..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/security_product_features_config.ts +++ /dev/null @@ -1,136 +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 { - ProductFeatureKeys, - ProductFeatureKibanaConfig, - ProductFeaturesSecurityConfig, -} from '@kbn/security-solution-features'; -import { - ProductFeatureSecurityKey, - SecuritySubFeatureId, -} from '@kbn/security-solution-features/keys'; -import { - securityDefaultProductFeaturesConfig, - createEnabledProductFeaturesConfigMap, -} from '@kbn/security-solution-features/config'; -import { SECURITY_FEATURE_ID_V3 } from '@kbn/security-solution-features/constants'; -import { APP_ID } from '@kbn/security-solution-plugin/common'; - -export const getSecurityProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesSecurityConfig => { - return createEnabledProductFeaturesConfigMap( - securityProductFeaturesConfig, - enabledProductFeatureKeys - ); - }; - -/** - * 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 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. - */ -const securityProductFeaturesConfig: Record< - ProductFeatureSecurityKey, - ProductFeatureKibanaConfig -> = { - ...securityDefaultProductFeaturesConfig, - [ProductFeatureSecurityKey.endpointExceptions]: { - privileges: { - all: { - ui: ['showEndpointExceptions', 'crudEndpointExceptions'], - api: [`${APP_ID}-showEndpointExceptions`, `${APP_ID}-crudEndpointExceptions`], - }, - read: { - ui: ['showEndpointExceptions'], - api: [`${APP_ID}-showEndpointExceptions`], - }, - }, - }, - - [ProductFeatureSecurityKey.endpointArtifactManagement]: { - subFeatureIds: [ - SecuritySubFeatureId.hostIsolationExceptionsBasic, - SecuritySubFeatureId.trustedApplications, - SecuritySubFeatureId.blocklist, - SecuritySubFeatureId.eventFilters, - SecuritySubFeatureId.globalArtifactManagement, - ], - - baseFeatureConfigModifier: (baseFeatureConfig) => { - if ( - !['siem', 'siemV2'].includes(baseFeatureConfig.id) || - !baseFeatureConfig.privileges?.all.replacedBy || - !('default' in baseFeatureConfig.privileges.all.replacedBy) - ) { - return baseFeatureConfig; - } - - return { - ...baseFeatureConfig, - privileges: { - ...baseFeatureConfig.privileges, - - all: { - ...baseFeatureConfig.privileges.all, - - // overwriting siem:ALL role migration in siem and siemV2 - replacedBy: { - default: baseFeatureConfig.privileges.all.replacedBy.default.map( - (privilegesPreference) => { - if (privilegesPreference.feature === SECURITY_FEATURE_ID_V3) { - return { - feature: SECURITY_FEATURE_ID_V3, - privileges: [ - // Enabling sub-features toggle to show that Global Artifact Management is now provided to the user. - 'minimal_all', - - // Writing global (not per-policy) Artifacts is gated with Global Artifact Management:ALL starting with siemV3. - // Users who have been able to write ANY Artifact before are now granted with this privilege to keep existing behavior. - // This migration is for Endpoint Exceptions artifact in ESS offering, as it included in Security:ALL privilege. - 'global_artifact_management_all', - ], - }; - } - - return privilegesPreference; - } - ), - - minimal: baseFeatureConfig.privileges.all.replacedBy.minimal.map( - (privilegesPreference) => { - if (privilegesPreference.feature === SECURITY_FEATURE_ID_V3) { - return { - feature: SECURITY_FEATURE_ID_V3, - privileges: [ - 'minimal_all', - - // on ESS, Endpoint Exception ALL is included in siem:MINIMAL_ALL - 'global_artifact_management_all', - ], - }; - } - - return privilegesPreference; - } - ), - }, - api: [ - ...(baseFeatureConfig.privileges.all.api ?? []), - - // API access must be also added, as only UI privileges are copied when replacing a deprecated feature - `${APP_ID}-writeGlobalArtifacts`, - ], - }, - }, - }; - }, - }, -}; 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 deleted file mode 100644 index 6e42f7009cdbf..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/siem_migrations_product_features_config.ts +++ /dev/null @@ -1,41 +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 { - 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_ess/server/product_features/timeline_product_features_config.ts b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/timeline_product_features_config.ts deleted file mode 100644 index 411c06ecfe75f..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/timeline_product_features_config.ts +++ /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 type { - ProductFeatureKeys, - ProductFeatureKibanaConfig, - ProductFeaturesTimelineConfig, -} from '@kbn/security-solution-features'; -import { - createEnabledProductFeaturesConfigMap, - timelineDefaultProductFeaturesConfig, -} from '@kbn/security-solution-features/config'; -import type { ProductFeatureTimelineFeatureKey } 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 timelineProductFeaturesConfig: Record< - ProductFeatureTimelineFeatureKey, - ProductFeatureKibanaConfig -> = { - ...timelineDefaultProductFeaturesConfig, - // ess-specific app features configs here -}; - -export const getTimelineProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesTimelineConfig => - createEnabledProductFeaturesConfigMap(timelineProductFeaturesConfig, enabledProductFeatureKeys); diff --git a/x-pack/solutions/security/plugins/security_solution_ess/tsconfig.json b/x-pack/solutions/security/plugins/security_solution_ess/tsconfig.json index 23d9ec2f31c3b..49a6f8aaebb66 100644 --- a/x-pack/solutions/security/plugins/security_solution_ess/tsconfig.json +++ b/x-pack/solutions/security/plugins/security_solution_ess/tsconfig.json @@ -18,7 +18,6 @@ "@kbn/security-solution-plugin", "@kbn/kibana-react-plugin", "@kbn/security-solution-features", - "@kbn/cases-plugin", "@kbn/security-solution-navigation", "@kbn/licensing-plugin", "@kbn/security-solution-upselling", diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/plugin.ts index 848994857dc88..e3fc015ad971c 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution_serverless/server/plugin.ts @@ -85,7 +85,7 @@ export class SecuritySolutionServerlessPlugin // Register product features const enabledProductFeatures = getEnabledProductFeatures(this.config.productTypes); - registerProductFeatures(pluginsSetup, enabledProductFeatures, this.config); + registerProductFeatures(pluginsSetup, enabledProductFeatures); // Register telemetry events telemetryEvents.forEach((eventConfig) => coreSetup.analytics.registerEventType(eventConfig)); diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/assistant_product_features_config.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/assistant_product_features_config.ts deleted file mode 100644 index 27feabee5c0fd..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/assistant_product_features_config.ts +++ /dev/null @@ -1,44 +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 { - ProductFeatureKeys, - ProductFeatureKibanaConfig, - ProductFeaturesAssistantConfig, -} from '@kbn/security-solution-features'; -import { - assistantDefaultProductFeaturesConfig, - createEnabledProductFeaturesConfigMap, -} from '@kbn/security-solution-features/config'; -import type { - ProductFeatureAssistantKey, - AssistantSubFeatureId, -} from '@kbn/security-solution-features/keys'; - -export const getSecurityAssistantProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesAssistantConfig => { - return createEnabledProductFeaturesConfigMap( - assistantProductFeaturesConfig, - enabledProductFeatureKeys - ); - }; - -/** - * Maps the ProductFeatures keys to Kibana privileges that will be merged - * into the base privileges config for the Security Assistant app. - * - * Privileges can be added in different ways: - * - `privileges`: the privileges that will be added directly into the main Security Assistant feature. - * - `subFeatureIds`: the ids of the sub-features that will be added into the Assistant subFeatures entry. - * - `subFeaturesPrivileges`: the privileges that will be added into the existing Assistant subFeature with the privilege `id` specified. - */ -const assistantProductFeaturesConfig: Record< - ProductFeatureAssistantKey, - ProductFeatureKibanaConfig -> = { - ...assistantDefaultProductFeaturesConfig, - // serverless-specific app features configs here -}; diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/attack_discovery_product_features_config.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/attack_discovery_product_features_config.ts deleted file mode 100644 index 406c396edfb72..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/attack_discovery_product_features_config.ts +++ /dev/null @@ -1,40 +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 { - ProductFeatureKeys, - ProductFeatureKibanaConfig, - ProductFeaturesAttackDiscoveryConfig, -} from '@kbn/security-solution-features'; -import { - attackDiscoveryDefaultProductFeaturesConfig, - createEnabledProductFeaturesConfigMap, -} from '@kbn/security-solution-features/config'; -import type { ProductFeatureAttackDiscoveryKey } 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 attackDiscoveryProductFeaturesConfig: Record< - ProductFeatureAttackDiscoveryKey, - ProductFeatureKibanaConfig -> = { - ...attackDiscoveryDefaultProductFeaturesConfig, - // serverless-specific app features configs here -}; - -export const getAttackDiscoveryProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesAttackDiscoveryConfig => - createEnabledProductFeaturesConfigMap( - attackDiscoveryProductFeaturesConfig, - enabledProductFeatureKeys - ); diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/cases_product_features_config.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/cases_product_features_config.ts deleted file mode 100644 index 21c1fa88e56cb..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/cases_product_features_config.ts +++ /dev/null @@ -1,51 +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 { - ProductFeatureKibanaConfig, - ProductFeaturesCasesConfig, - ProductFeatureKeys, -} from '@kbn/security-solution-features'; -import { - getCasesDefaultProductFeaturesConfig, - createEnabledProductFeaturesConfigMap, -} from '@kbn/security-solution-features/config'; -import type { - ProductFeatureCasesKey, - CasesSubFeatureId, -} from '@kbn/security-solution-features/keys'; -import { - CASES_CONNECTORS_CAPABILITY, - GET_CONNECTORS_CONFIGURE_API_TAG, -} from '@kbn/cases-plugin/common/constants'; - -export const getCasesProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesCasesConfig => { - return createEnabledProductFeaturesConfigMap( - casesProductFeaturesConfig, - enabledProductFeatureKeys - ); - }; - -/** - * Maps the ProductFeatures keys to Kibana privileges that will be merged - * into the base privileges config for the Security Cases app. - * - * Privileges can be added in different ways: - * - `privileges`: the privileges that will be added directly into the main Security Cases feature. - * - `subFeatureIds`: the ids of the sub-features that will be added into the Cases subFeatures entry. - * - `subFeaturesPrivileges`: the privileges that will be added into the existing Cases subFeature with the privilege `id` specified. - */ -const casesProductFeaturesConfig: Record< - ProductFeatureCasesKey, - ProductFeatureKibanaConfig -> = { - ...getCasesDefaultProductFeaturesConfig({ - apiTags: { connectors: GET_CONNECTORS_CONFIGURE_API_TAG }, - uiCapabilities: { connectors: CASES_CONNECTORS_CAPABILITY }, - }), - // serverless-specific app features configs here -}; 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 43ba3a75edf72..5dfe77d6d10c3 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,22 +9,15 @@ import type { Logger } from '@kbn/logging'; import { ProductFeatureKey } from '@kbn/security-solution-features/keys'; import type { ProductFeatureKeys } from '@kbn/security-solution-features'; -import { getCasesProductFeaturesConfigurator } from './cases_product_features_config'; -import { getSecurityProductFeaturesConfigurator } from './security_product_features_config'; -import { getSecurityAssistantProductFeaturesConfigurator } from './assistant_product_features_config'; -import { getAttackDiscoveryProductFeaturesConfigurator } from './attack_discovery_product_features_config'; -import { getTimelineProductFeaturesConfigurator } from './timeline_product_features_config'; -import { getNotesProductFeaturesConfigurator } from './notes_product_features_config'; -import { getSiemMigrationsProductFeaturesConfigurator } from './siem_migrations_product_features_config'; import { enableRuleActions } from '../rules/enable_rule_actions'; import type { ServerlessSecurityConfig } from '../config'; import type { Tier, SecuritySolutionServerlessPluginSetupDeps } from '../types'; import { ProductLine } from '../../common/product'; +import { productFeaturesExtensions } from './product_features_extensions'; export const registerProductFeatures = ( pluginsSetup: SecuritySolutionServerlessPluginSetupDeps, - enabledProductFeatureKeys: ProductFeatureKeys, - config: ServerlessSecurityConfig + enabledProductFeatureKeys: ProductFeatureKeys ): void => { // securitySolutionEss plugin should always be disabled when securitySolutionServerless is enabled. // This check is an additional layer of security to prevent double registrations when @@ -36,16 +29,8 @@ export const registerProductFeatures = ( // register product features for the main security solution product features service pluginsSetup.securitySolution.setProductFeaturesConfigurator({ - security: getSecurityProductFeaturesConfigurator( - enabledProductFeatureKeys, - config.experimentalFeatures - ), - cases: getCasesProductFeaturesConfigurator(enabledProductFeatureKeys), - securityAssistant: getSecurityAssistantProductFeaturesConfigurator(enabledProductFeatureKeys), - attackDiscovery: getAttackDiscoveryProductFeaturesConfigurator(enabledProductFeatureKeys), - timeline: getTimelineProductFeaturesConfigurator(enabledProductFeatureKeys), - notes: getNotesProductFeaturesConfigurator(enabledProductFeatureKeys), - siemMigrations: getSiemMigrationsProductFeaturesConfigurator(enabledProductFeatureKeys), + enabledProductFeatureKeys, + extensions: productFeaturesExtensions, }); // enable rule actions based on the enabled product features diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/notes_product_features_config.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/notes_product_features_config.ts deleted file mode 100644 index 1b00761f6da4a..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/notes_product_features_config.ts +++ /dev/null @@ -1,37 +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 { - ProductFeatureKeys, - ProductFeatureKibanaConfig, - ProductFeaturesNotesConfig, -} from '@kbn/security-solution-features'; -import { - notesDefaultProductFeaturesConfig, - createEnabledProductFeaturesConfigMap, -} from '@kbn/security-solution-features/config'; -import type { ProductFeatureNotesFeatureKey } 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 notesProductFeaturesConfig: Record< - ProductFeatureNotesFeatureKey, - ProductFeatureKibanaConfig -> = { - ...notesDefaultProductFeaturesConfig, - // serverless-specific app features configs here -}; - -export const getNotesProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesNotesConfig => - createEnabledProductFeaturesConfigMap(notesProductFeaturesConfig, enabledProductFeatureKeys); diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/product_features_extensions.test.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/product_features_extensions.test.ts new file mode 100644 index 0000000000000..d6d5318776b90 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/product_features_extensions.test.ts @@ -0,0 +1,114 @@ +/* + * 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 { updateGlobalArtifactManageReplacements } from './product_features_extensions'; +import { SECURITY_FEATURE_ID_V3 } from '@kbn/security-solution-features/constants'; +import type { MutableKibanaFeatureConfig } from '@kbn/security-solution-features'; +import { cloneDeep } from 'lodash'; + +const baseFeatureConfig: MutableKibanaFeatureConfig = { + id: 'siem', + name: 'Security Feature', + app: ['securitySolution'], + category: { id: 'security', label: 'Security' }, + privileges: { + all: { + savedObject: { + all: ['*'], + read: ['*'], + }, + ui: ['all'], + api: [`${SECURITY_FEATURE_ID_V3}-all`], + }, + read: { + savedObject: { + all: ['*'], + read: ['*'], + }, + ui: ['read'], + api: [`${SECURITY_FEATURE_ID_V3}-read`], + }, + }, +}; + +describe('updateGlobalArtifactManageReplacements', () => { + let featureConfig: MutableKibanaFeatureConfig; + + beforeEach(() => { + featureConfig = cloneDeep(baseFeatureConfig); + }); + + it('should do nothing if replacedBy is not present', () => { + const originalConfig = JSON.parse(JSON.stringify(featureConfig)); + + updateGlobalArtifactManageReplacements(featureConfig as MutableKibanaFeatureConfig); + + expect(featureConfig).toEqual(originalConfig); + }); + + it('should modify privileges for SECURITY_FEATURE_ID_V3 in both default and minimal', () => { + const testFeatureConfig = { + ...featureConfig, + privileges: { + ...featureConfig.privileges, + all: { + ...featureConfig.privileges?.all, + replacedBy: { + default: [ + { feature: SECURITY_FEATURE_ID_V3, privileges: ['all'] }, + { feature: 'other_feature', privileges: ['all'] }, + ], + minimal: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['all'] }], + }, + }, + }, + }; + + updateGlobalArtifactManageReplacements(testFeatureConfig as MutableKibanaFeatureConfig); + + const replacedBy = testFeatureConfig.privileges.all.replacedBy; + + // Default privileges modified + const v3Default = replacedBy.default.find( + ({ feature }: { feature: string }) => feature === SECURITY_FEATURE_ID_V3 + ); + expect(v3Default?.privileges).toEqual([ + 'minimal_all', + 'global_artifact_management_all', + 'endpoint_exceptions_all', + ]); + + // Ensure other features remain unchanged + const otherFeature = replacedBy.default.find( + ({ feature }: { feature: string }) => feature === 'other_feature' + ); + expect(otherFeature?.privileges).toEqual(['all']); + }); + + it('should only modify existing SECURITY_FEATURE_ID_V3 entries', () => { + const testFeatureConfig = { + ...featureConfig, + privileges: { + ...featureConfig.privileges, + all: { + ...featureConfig.privileges?.all, + replacedBy: { + default: [{ feature: 'other_feature', privileges: ['all'] }], + minimal: [{ feature: 'other_feature', privileges: ['all'] }], + }, + }, + }, + }; + + updateGlobalArtifactManageReplacements(testFeatureConfig as MutableKibanaFeatureConfig); + + const replacedBy = testFeatureConfig.privileges.all.replacedBy; + + // No SECURITY_FEATURE_ID_V3, so no changes + expect(replacedBy.default[0].privileges).toEqual(['all']); + expect(replacedBy.minimal[0].privileges).toEqual(['all']); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/product_features_extensions.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/product_features_extensions.ts new file mode 100644 index 0000000000000..d856a37769061 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/product_features_extensions.ts @@ -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 type { + MutableKibanaFeatureConfig, + ProductFeaturesConfiguratorExtensions, +} from '@kbn/security-solution-features'; +import { SECURITY_FEATURE_ID_V3 } from '@kbn/security-solution-features/constants'; +import { + ProductFeatureSecurityKey, + SecuritySubFeatureId, +} from '@kbn/security-solution-features/keys'; + +export const productFeaturesExtensions: ProductFeaturesConfiguratorExtensions = { + security: { + allVersions: { + [ProductFeatureSecurityKey.endpointExceptions]: { + subFeatureIds: [SecuritySubFeatureId.endpointExceptions], + }, + }, + version: { + siem: { + [ProductFeatureSecurityKey.endpointArtifactManagement]: { + featureConfigModifiers: [updateGlobalArtifactManageReplacements], + }, + }, + siemV2: { + [ProductFeatureSecurityKey.endpointArtifactManagement]: { + featureConfigModifiers: [updateGlobalArtifactManageReplacements], + }, + }, + }, + }, +}; + +// When endpointArtifactManagement PLI is enabled, the replacedBy to the siemV3 feature needs to +// account for the privileges of the additional sub-features that it introduces, migrating them correctly. +// This needs to be done here because the replacements of serverless and ESS are different. +export function updateGlobalArtifactManageReplacements( + featureConfig: MutableKibanaFeatureConfig +): void { + const replacedBy = featureConfig.privileges?.all?.replacedBy; + if (!replacedBy || !('default' in replacedBy)) { + return; + } + // only "default" is overwritten, "minimal" is not as it does not includes Endpoint Exceptions ALL. + const v3Default = replacedBy.default.find( + ({ feature }) => feature === SECURITY_FEATURE_ID_V3 // Only for features that are replaced by siemV3 (siem and siemV2) + ); + if (v3Default) { + // Override replaced privileges from `all` to `minimal_all` with additional sub-features privileges + v3Default.privileges = [ + 'minimal_all', + // Writing global (not per-policy) Artifacts is gated with Global Artifact Management:ALL starting with siemV3. + // Users who have been able to write ANY Artifact before are now granted with this privilege to keep existing behavior. + // This migration is for Endpoint Exceptions artifact in Serverless offering, as it included in Security:ALL privilege. + 'global_artifact_management_all', + // As we are switching from `all` to `minimal_all`, Endpoint Exceptions is needed to be added, as it was included in `all`, + // but not in `minimal_all`. + 'endpoint_exceptions_all', + ]; + } +} diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/security_product_features_config.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/security_product_features_config.ts deleted file mode 100644 index 4b4fe72a13ac1..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/security_product_features_config.ts +++ /dev/null @@ -1,123 +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 { - ProductFeatureKeys, - ProductFeatureKibanaConfig, - ProductFeaturesSecurityConfig, -} from '@kbn/security-solution-features'; -import { - securityDefaultProductFeaturesConfig, - createEnabledProductFeaturesConfigMap, -} from '@kbn/security-solution-features/config'; -import { - ProductFeatureSecurityKey, - SecuritySubFeatureId, -} from '@kbn/security-solution-features/keys'; -import { SECURITY_FEATURE_ID_V3 } from '@kbn/security-solution-features/constants'; -import { APP_ID } from '@kbn/security-solution-plugin/common'; -import type { ExperimentalFeatures } from '../../common/experimental_features'; - -export const getSecurityProductFeaturesConfigurator = - ( - enabledProductFeatureKeys: ProductFeatureKeys, - _: ExperimentalFeatures // currently un-used, but left here as a convenience for possible future use - ) => - (): ProductFeaturesSecurityConfig => { - return createEnabledProductFeaturesConfigMap( - securityProductFeaturesConfig, - enabledProductFeatureKeys - ); - }; - -/** - * 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 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. - */ -const securityProductFeaturesConfig: Record< - ProductFeatureSecurityKey, - ProductFeatureKibanaConfig -> = { - ...securityDefaultProductFeaturesConfig, - [ProductFeatureSecurityKey.endpointExceptions]: { - subFeatureIds: [SecuritySubFeatureId.endpointExceptions], - }, - - [ProductFeatureSecurityKey.endpointArtifactManagement]: { - subFeatureIds: [ - SecuritySubFeatureId.hostIsolationExceptionsBasic, - SecuritySubFeatureId.trustedApplications, - SecuritySubFeatureId.blocklist, - SecuritySubFeatureId.eventFilters, - SecuritySubFeatureId.globalArtifactManagement, - ], - - baseFeatureConfigModifier: (baseFeatureConfig) => { - if ( - !['siem', 'siemV2'].includes(baseFeatureConfig.id) || - !baseFeatureConfig.privileges?.all.replacedBy || - !('default' in baseFeatureConfig.privileges.all.replacedBy) - ) { - return baseFeatureConfig; - } - - return { - ...baseFeatureConfig, - privileges: { - ...baseFeatureConfig.privileges, - - all: { - ...baseFeatureConfig.privileges.all, - - // overwriting siem:ALL role migration in siem and siemV2 - replacedBy: { - ...baseFeatureConfig.privileges.all.replacedBy, - - default: baseFeatureConfig.privileges.all.replacedBy.default.map( - (privilegesPreference) => { - if (privilegesPreference.feature === SECURITY_FEATURE_ID_V3) { - return { - feature: SECURITY_FEATURE_ID_V3, - privileges: [ - // Enabling sub-features toggle to show that Global Artifact Management is now provided to the user. - 'minimal_all', - - // Writing global (not per-policy) Artifacts is gated with Global Artifact Management:ALL starting with siemV3. - // Users who have been able to write ANY Artifact before are now granted with this privilege to keep existing behavior. - // This migration is for Endpoint Exceptions artifact in Serverless offering, as it included in Security:ALL privilege. - 'global_artifact_management_all', - - // As we are switching from `all` to `minimal_all`, Endpoint Exceptions is needed to be added, as it was included in `all`, - // but not in `minimal_all`. - 'endpoint_exceptions_all', - ], - }; - } - - return privilegesPreference; - } - ), - }, - - api: [ - ...(baseFeatureConfig.privileges.all.api ?? []), - - // API access must be also added, as only UI privileges are copied when replacing a deprecated feature - `${APP_ID}-writeGlobalArtifacts`, - ], - - // minimal_all is not overwritten, as it does not includes Endpoint Exceptions ALL. - }, - }, - }; - }, - }, -}; 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 deleted file mode 100644 index b6bcb93c8f8ad..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/siem_migrations_product_features_config.ts +++ /dev/null @@ -1,40 +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 { - ProductFeatureKeys, - ProductFeatureKibanaConfig, - ProductFeaturesSiemMigrationsConfig, -} from '@kbn/security-solution-features'; -import { - siemMigrationsDefaultProductFeaturesConfig, - createEnabledProductFeaturesConfigMap, -} from '@kbn/security-solution-features/config'; -import type { ProductFeatureSiemMigrationsKey } from '@kbn/security-solution-features/keys'; - -/** - * Maps the ProductFeatures keys to Kibana privileges that will be merged - * into the base privileges config for the app. - * - * Privileges can be added in different ways: - * - `privileges`: the privileges that will be added directly into the main Attack discovery feature. - * - `subFeatureIds`: the ids of the sub-features that will be added into the Attack discovery subFeatures entry. - * - `subFeaturesPrivileges`: the privileges that will be added into the existing Attack discovery subFeature with the privilege `id` specified. - */ -const siemMigrationsProductFeaturesConfig: Record< - ProductFeatureSiemMigrationsKey, - ProductFeatureKibanaConfig -> = { - ...siemMigrationsDefaultProductFeaturesConfig, - // serverless-specific app features configs here -}; - -export const getSiemMigrationsProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesSiemMigrationsConfig => - createEnabledProductFeaturesConfigMap( - siemMigrationsProductFeaturesConfig, - enabledProductFeatureKeys - ); diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/timeline_product_features_config.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/timeline_product_features_config.ts deleted file mode 100644 index fde220b83cb30..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/timeline_product_features_config.ts +++ /dev/null @@ -1,37 +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 { - ProductFeatureKeys, - ProductFeatureKibanaConfig, - ProductFeaturesTimelineConfig, -} from '@kbn/security-solution-features'; -import { - timelineDefaultProductFeaturesConfig, - createEnabledProductFeaturesConfigMap, -} from '@kbn/security-solution-features/config'; -import type { ProductFeatureTimelineFeatureKey } 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 timelineProductFeaturesConfig: Record< - ProductFeatureTimelineFeatureKey, - ProductFeatureKibanaConfig -> = { - ...timelineDefaultProductFeaturesConfig, - // serverless-specific app features configs here -}; - -export const getTimelineProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesTimelineConfig => - createEnabledProductFeaturesConfigMap(timelineProductFeaturesConfig, enabledProductFeatureKeys); diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/tsconfig.json b/x-pack/solutions/security/plugins/security_solution_serverless/tsconfig.json index b46a6f2e2c9fe..4f11ba997007d 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/tsconfig.json +++ b/x-pack/solutions/security/plugins/security_solution_serverless/tsconfig.json @@ -34,7 +34,6 @@ "@kbn/cloud-plugin", "@kbn/cloud-security-posture-plugin", "@kbn/security-solution-features", - "@kbn/cases-plugin", "@kbn/fleet-plugin", "@kbn/serverless-security-settings", "@kbn/core-elasticsearch-server", diff --git a/x-pack/solutions/security/test/serverless/api_integration/test_suites/platform_security/authorization.ts b/x-pack/solutions/security/test/serverless/api_integration/test_suites/platform_security/authorization.ts index 172e4ac6b4e70..a93094ac4192e 100644 --- a/x-pack/solutions/security/test/serverless/api_integration/test_suites/platform_security/authorization.ts +++ b/x-pack/solutions/security/test/serverless/api_integration/test_suites/platform_security/authorization.ts @@ -1229,9 +1229,9 @@ export default function ({ getService }: FtrProviderContext) { "api:lists-summary", "api:securitySolution-deleteHostIsolationExceptions", "api:securitySolution-readHostIsolationExceptions", - "api:securitySolution-writeGlobalArtifacts", "api:securitySolution-accessHostIsolationExceptions", "api:securitySolution-writeHostIsolationExceptions", + "api:securitySolution-writeGlobalArtifacts", "saved_object:exception-list-agnostic/bulk_get", "saved_object:exception-list-agnostic/get", "saved_object:exception-list-agnostic/find", @@ -3930,9 +3930,9 @@ export default function ({ getService }: FtrProviderContext) { "api:lists-summary", "api:securitySolution-deleteHostIsolationExceptions", "api:securitySolution-readHostIsolationExceptions", - "api:securitySolution-writeGlobalArtifacts", "api:securitySolution-accessHostIsolationExceptions", "api:securitySolution-writeHostIsolationExceptions", + "api:securitySolution-writeGlobalArtifacts", "saved_object:exception-list-agnostic/bulk_get", "saved_object:exception-list-agnostic/get", "saved_object:exception-list-agnostic/find",