From db444a9b4c3cd15f9917ec54d36036d9d50d613d Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Wed, 23 Jul 2025 14:12:03 +0200 Subject: [PATCH 01/28] small refactor --- .../security/packages/features/src/types.ts | 5 +- .../product_features_config_merger.ts | 9 +- .../security_product_features_config.ts | 103 +++++++----------- .../security_product_features_config.ts | 87 +++++---------- 4 files changed, 78 insertions(+), 126 deletions(-) diff --git a/x-pack/solutions/security/packages/features/src/types.ts b/x-pack/solutions/security/packages/features/src/types.ts index 1d7dd25455bf6..4eb9d8ca4f06f 100644 --- a/x-pack/solutions/security/packages/features/src/types.ts +++ b/x-pack/solutions/security/packages/features/src/types.ts @@ -36,7 +36,10 @@ export type ProductFeatureKibanaConfig = subFeatureIds?: T[]; subFeaturesPrivileges?: SubFeaturesPrivileges[]; - /** An option for product features to modify the base kibana feature. + /** + * Optional function for custom modification of the base feature config. + * This can be used to apply specific changes to the base feature config before merging it with + * the rest of the product feature configurations. * * @param baseFeatureConfig * @returns modified baseFeatureConfig 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..7c559f505f4f3 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 @@ -50,13 +50,14 @@ export class ProductFeaturesConfigMerger { enabledSubFeaturesIndexed[subFeatureId] = true; }); - if (baseFeatureConfigModifier) { - mergedKibanaFeatureConfig = baseFeatureConfigModifier(mergedKibanaFeatureConfig); - } - if (subFeaturesPrivileges) { subFeaturesPrivilegesToMerge.push(...subFeaturesPrivileges); } + + // apply custom modifications before merging + if (baseFeatureConfigModifier) { + mergedKibanaFeatureConfig = baseFeatureConfigModifier(mergedKibanaFeatureConfig); + } mergeWith(mergedKibanaFeatureConfig, productFeatureConfigToMerge, featureConfigMerger); }); 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 index 955152b1c84fc..8c65dfb173d99 100644 --- 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 @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import immer from 'immer'; import type { ProductFeatureKeys, ProductFeatureKibanaConfig, @@ -56,6 +57,10 @@ const securityProductFeaturesConfig: Record< }, [ProductFeatureSecurityKey.endpointArtifactManagement]: { + privileges: { + all: { api: [`${APP_ID}-writeGlobalArtifacts`] }, + }, + subFeatureIds: [ SecuritySubFeatureId.hostIsolationExceptionsBasic, SecuritySubFeatureId.trustedApplications, @@ -64,73 +69,43 @@ const securityProductFeaturesConfig: Record< SecuritySubFeatureId.globalArtifactManagement, ], + // When endpointArtifactManagement PLI is enabled, the replacedBy for the siemV3 feature needs to + // account for the privileges of the sub-features that are introduced by it. 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 immer(baseFeatureConfig, (draft) => { + const replacedBy = draft.privileges?.all?.replacedBy; + if (!replacedBy) { + return; + } - return privilegesPreference; - } - ), - }, - api: [ - ...(baseFeatureConfig.privileges.all.api ?? []), + const defaultReplacedBy = Array.isArray(replacedBy) ? replacedBy : replacedBy.default; + if (defaultReplacedBy) { + const v3Default = defaultReplacedBy.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', + 'global_artifact_management_all', // Enabling sub-features toggle to show that Global Artifact Management is now provided to the user. + ]; + } + } - // API access must be also added, as only UI privileges are copied when replacing a deprecated feature - `${APP_ID}-writeGlobalArtifacts`, - ], - }, - }, - }; + const minimalReplacedBy = Array.isArray(replacedBy) ? undefined : replacedBy.minimal; + if (minimalReplacedBy) { + const v3Minimal = minimalReplacedBy.find( + ({ feature }) => feature === SECURITY_FEATURE_ID_V3 // Only for features that are replaced by siemV3 (siem and siemV2) + ); + 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_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 index 4b4fe72a13ac1..2a29530db860b 100644 --- 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 @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import immer from 'immer'; import type { ProductFeatureKeys, ProductFeatureKibanaConfig, @@ -52,6 +53,10 @@ const securityProductFeaturesConfig: Record< }, [ProductFeatureSecurityKey.endpointArtifactManagement]: { + privileges: { + all: { api: [`${APP_ID}-writeGlobalArtifacts`] }, + }, + subFeatureIds: [ SecuritySubFeatureId.hostIsolationExceptionsBasic, SecuritySubFeatureId.trustedApplications, @@ -60,64 +65,32 @@ const securityProductFeaturesConfig: Record< SecuritySubFeatureId.globalArtifactManagement, ], + // When endpointArtifactManagement PLI is enabled, the replacedBy for the siemV3 feature needs to + // account for the privileges of the sub-features that are introduced by it. 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. - }, - }, - }; + return immer(baseFeatureConfig, (draft) => { + const replacedBy = draft.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', + ]; + } + }); }, }, }; From 51779a16adc229bab4eeddc4b1eb36df7dd61fba Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Wed, 23 Jul 2025 16:31:12 +0200 Subject: [PATCH 02/28] rely on (safe) mutations --- .../security/packages/features/src/types.ts | 14 ++--- .../product_features_config_merger.ts | 4 +- .../security_product_features_config.ts | 57 +++++++++---------- .../security_product_features_config.ts | 45 +++++++-------- 4 files changed, 55 insertions(+), 65 deletions(-) diff --git a/x-pack/solutions/security/packages/features/src/types.ts b/x-pack/solutions/security/packages/features/src/types.ts index 4eb9d8ca4f06f..1dd99611913cb 100644 --- a/x-pack/solutions/security/packages/features/src/types.ts +++ b/x-pack/solutions/security/packages/features/src/types.ts @@ -37,16 +37,14 @@ export type ProductFeatureKibanaConfig = subFeaturesPrivileges?: SubFeaturesPrivileges[]; /** - * Optional function for custom modification of the base feature config. - * This can be used to apply specific changes to the base feature config before merging it with - * the rest of the product feature configurations. + * Optional function to apply custom modifications to the base feature config, for specific ProductFeatures. + * The base config received is a clone of the original KibanaFeatureConfig, so it can be mutated safely. + * The modifications are applied before applying the rest of the properties of the ProductFeatureConfigs. * - * @param baseFeatureConfig - * @returns modified baseFeatureConfig + * @param baseFeatureConfig to be mutated + * @returns void */ - baseFeatureConfigModifier?: ( - baseFeatureConfig: BaseKibanaFeatureConfig - ) => BaseKibanaFeatureConfig; + baseFeatureConfigModifier?: (baseFeatureConfig: BaseKibanaFeatureConfig) => void; }; export type ProductFeaturesConfig = Map< ProductFeatureKeyType, 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 7c559f505f4f3..851f7b3f707fb 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 @@ -32,7 +32,7 @@ export class ProductFeaturesConfigMerger { kibanaSubFeatureIds: T[], productFeaturesConfigs: ProductFeatureKibanaConfig[] ): KibanaFeatureConfig { - let mergedKibanaFeatureConfig = cloneDeep(kibanaFeatureConfig) as KibanaFeatureConfig; + const mergedKibanaFeatureConfig = cloneDeep(kibanaFeatureConfig) as KibanaFeatureConfig; const subFeaturesPrivilegesToMerge: SubFeaturesPrivileges[] = []; const enabledSubFeaturesIndexed = Object.fromEntries( kibanaSubFeatureIds.map((id) => [id, true]) @@ -56,7 +56,7 @@ export class ProductFeaturesConfigMerger { // apply custom modifications before merging if (baseFeatureConfigModifier) { - mergedKibanaFeatureConfig = baseFeatureConfigModifier(mergedKibanaFeatureConfig); + baseFeatureConfigModifier(mergedKibanaFeatureConfig); } mergeWith(mergedKibanaFeatureConfig, productFeatureConfigToMerge, featureConfigMerger); }); 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 index 8c65dfb173d99..200ff377c6596 100644 --- 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 @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import immer from 'immer'; import type { ProductFeatureKeys, ProductFeatureKibanaConfig, @@ -72,40 +71,36 @@ const securityProductFeaturesConfig: Record< // When endpointArtifactManagement PLI is enabled, the replacedBy for the siemV3 feature needs to // account for the privileges of the sub-features that are introduced by it. baseFeatureConfigModifier: (baseFeatureConfig) => { - return immer(baseFeatureConfig, (draft) => { - const replacedBy = draft.privileges?.all?.replacedBy; - if (!replacedBy) { - return; - } + const replacedBy = baseFeatureConfig.privileges?.all?.replacedBy; + if (!replacedBy) { + return; + } - const defaultReplacedBy = Array.isArray(replacedBy) ? replacedBy : replacedBy.default; - if (defaultReplacedBy) { - const v3Default = defaultReplacedBy.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', - 'global_artifact_management_all', // Enabling sub-features toggle to show that Global Artifact Management is now provided to the user. - ]; - } + if ('default' in replacedBy) { + 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', + 'global_artifact_management_all', // Enabling sub-features toggle to show that Global Artifact Management is now provided to the user. + ]; } + } - const minimalReplacedBy = Array.isArray(replacedBy) ? undefined : replacedBy.minimal; - if (minimalReplacedBy) { - const v3Minimal = minimalReplacedBy.find( - ({ feature }) => feature === SECURITY_FEATURE_ID_V3 // Only for features that are replaced by siemV3 (siem and siemV2) - ); - 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 - ]; - } + if ('minimal' in replacedBy) { + const v3Minimal = replacedBy.minimal.find( + ({ feature }) => feature === SECURITY_FEATURE_ID_V3 // Only for features that are replaced by siemV3 (siem and siemV2) + ); + 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_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 index 2a29530db860b..61d61772dc613 100644 --- 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 @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import immer from 'immer'; import type { ProductFeatureKeys, ProductFeatureKibanaConfig, @@ -68,29 +67,27 @@ const securityProductFeaturesConfig: Record< // When endpointArtifactManagement PLI is enabled, the replacedBy for the siemV3 feature needs to // account for the privileges of the sub-features that are introduced by it. baseFeatureConfigModifier: (baseFeatureConfig) => { - return immer(baseFeatureConfig, (draft) => { - const replacedBy = draft.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', - ]; - } - }); + const replacedBy = baseFeatureConfig.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', + ]; + } }, }, }; From 7208422b28d07bfe4e0abf2c6f90a31f3ef2de31 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Thu, 24 Jul 2025 13:54:02 +0200 Subject: [PATCH 03/28] code simplification --- .../src/assistant/product_feature_config.ts | 6 +- .../product_feature_config.ts | 22 ++--- .../packages/features/src/cases/types.ts | 6 +- .../security/packages/features/src/helpers.ts | 19 ++-- .../src/notes/product_feature_config.ts | 30 +++--- .../src/security/product_feature_config.ts | 16 +--- .../packages/features/src/security/types.ts | 10 +- .../siem_migrations/product_feature_config.ts | 22 ++--- .../src/timeline/product_feature_config.ts | 30 +++--- .../security/packages/features/src/types.ts | 56 +++++++---- .../product_features.ts | 22 ++--- .../product_features_config_merger.test.ts | 24 ++--- .../product_features_config_merger.ts | 18 +++- .../product_features_service.test.ts | 30 +++--- .../product_features_service.ts | 93 ++++--------------- .../security_solution_ess/common/constants.ts | 6 +- .../assistant_product_features_config.ts | 4 +- ...ttack_discovery_product_features_config.ts | 4 +- .../cases_product_features_config.ts | 4 +- .../notes_product_features_config.ts | 4 +- .../security_product_features_config.ts | 25 ++--- ...siem_migrations_product_features_config.ts | 4 +- .../timeline_product_features_config.ts | 4 +- .../endpoint_exceptions_details.tsx | 4 +- .../assistant_product_features_config.ts | 4 +- ...ttack_discovery_product_features_config.ts | 4 +- .../cases_product_features_config.ts | 4 +- .../notes_product_features_config.ts | 4 +- .../security_product_features_config.ts | 23 ++--- ...siem_migrations_product_features_config.ts | 4 +- .../timeline_product_features_config.ts | 4 +- 31 files changed, 208 insertions(+), 302 deletions(-) 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..272818315a9b6 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,7 +6,7 @@ */ 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. @@ -18,9 +18,9 @@ import type { ProductFeatureKibanaConfig } from '../types'; * - `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 assistantDefaultProductFeaturesConfig: ProductFeaturesConfig< ProductFeatureAssistantKey, - ProductFeatureKibanaConfig + AssistantSubFeatureId > = { [ProductFeatureAssistantKey.assistant]: { privileges: { 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..dafe8ba945761 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,7 +6,7 @@ */ 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. @@ -18,16 +18,14 @@ import type { ProductFeatureKibanaConfig } from '../types'; * - `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 attackDiscoveryDefaultProductFeaturesConfig: ProductFeaturesConfig = + { + [ProductFeatureAttackDiscoveryKey.attackDiscovery]: { + privileges: { + all: { + ui: ['attack-discovery'], + }, }, + subFeatureIds: [], }, - subFeatureIds: [], - }, -}; + }; 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..803ac3fbe24e8 100644 --- a/x-pack/solutions/security/packages/features/src/cases/types.ts +++ b/x-pack/solutions/security/packages/features/src/cases/types.ts @@ -6,7 +6,7 @@ */ 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; @@ -14,7 +14,7 @@ export interface CasesFeatureParams { savedObjects: { files: string[] }; } -export type DefaultCasesProductFeaturesConfig = Record< +export type DefaultCasesProductFeaturesConfig = ProductFeaturesConfig< ProductFeatureCasesKey, - ProductFeatureKibanaConfig + CasesSubFeatureId >; diff --git a/x-pack/solutions/security/packages/features/src/helpers.ts b/x-pack/solutions/security/packages/features/src/helpers.ts index 55baa89022c92..1d093920796fa 100644 --- a/x-pack/solutions/security/packages/features/src/helpers.ts +++ b/x-pack/solutions/security/packages/features/src/helpers.ts @@ -18,16 +18,17 @@ export const createEnabledProductFeaturesConfigMap = < K extends ProductFeatureKeyType, T extends string = string >( - productFeatures: Record>, + productFeatures: Partial>>, 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; - }, []) + Object.entries(productFeatures).reduce]>>( + (acc, [key, value]) => { + if (enabledProductFeaturesKeys.includes(key as K)) { + acc.push([key as K, value as ProductFeatureKibanaConfig]); + } + return acc; + }, + [] + ) ); 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..4107e88ed7dba 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 @@ -6,7 +6,7 @@ */ import { ProductFeatureNotesFeatureKey } from '../product_features_keys'; -import type { ProductFeatureKibanaConfig } from '../types'; +import type { ProductFeaturesConfig } from '../types'; /** * App features privileges configuration for the notes feature. @@ -18,20 +18,18 @@ import type { ProductFeatureKibanaConfig } from '../types'; * - `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]: { - privileges: { - all: { - api: ['notes_read', 'notes_write'], - ui: ['read', 'crud'], - }, - read: { - api: ['notes_read'], - ui: ['read'], +export const notesDefaultProductFeaturesConfig: ProductFeaturesConfig = + { + [ProductFeatureNotesFeatureKey.notes]: { + privileges: { + all: { + api: ['notes_read', 'notes_write'], + ui: ['read', 'crud'], + }, + read: { + api: ['notes_read'], + ui: ['read'], + }, }, }, - }, -}; + }; 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 d6ba5d5791428..9df712063b6de 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,7 +7,7 @@ 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. @@ -20,7 +20,7 @@ import type { DefaultSecurityProductFeaturesConfig } from './types'; * - `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: { @@ -164,16 +164,4 @@ 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]: {}, }; 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/siem_migrations/product_feature_config.ts b/x-pack/solutions/security/packages/features/src/siem_migrations/product_feature_config.ts index 9db1be79228cf..7487b935cbe95 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,7 +7,7 @@ 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. @@ -19,16 +19,14 @@ import type { ProductFeatureKibanaConfig } from '../types'; * - `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 siemMigrationsDefaultProductFeaturesConfig: ProductFeaturesConfig = + { + [ProductFeatureSiemMigrationsKey.siemMigrations]: { + privileges: { + all: { + api: [SIEM_MIGRATIONS_API_ACTION_ALL], + ui: ['all'], + }, }, }, - }, -}; + }; 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..a35955492a6e4 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 @@ -6,7 +6,7 @@ */ import { ProductFeatureTimelineFeatureKey } from '../product_features_keys'; -import type { ProductFeatureKibanaConfig } from '../types'; +import type { ProductFeaturesConfig } from '../types'; /** * App features privileges configuration for the timeline feature. @@ -18,20 +18,18 @@ import type { ProductFeatureKibanaConfig } from '../types'; * - `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]: { - privileges: { - all: { - api: ['timeline_read', 'timeline_write'], - ui: ['read', 'crud'], - }, - read: { - api: ['timeline_read'], - ui: ['read'], +export const timelineDefaultProductFeaturesConfig: ProductFeaturesConfig = + { + [ProductFeatureTimelineFeatureKey.timeline]: { + privileges: { + all: { + api: ['timeline_read', 'timeline_write'], + ui: ['read', 'crud'], + }, + read: { + api: ['timeline_read'], + ui: ['read'], + }, }, }, - }, -}; + }; diff --git a/x-pack/solutions/security/packages/features/src/types.ts b/x-pack/solutions/security/packages/features/src/types.ts index 1dd99611913cb..953373e6f2bcf 100644 --- a/x-pack/solutions/security/packages/features/src/types.ts +++ b/x-pack/solutions/security/packages/features/src/types.ts @@ -31,56 +31,72 @@ export type ProductFeatureKeys = ProductFeatureKeyType[]; // Features types export type BaseKibanaFeatureConfig = Omit; export type SubFeaturesPrivileges = RecursivePartial; + +export type FeatureConfigModifier = (baseFeatureConfig: BaseKibanaFeatureConfig) => 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[]; /** - * Optional function to apply custom modifications to the base feature config, for specific ProductFeatures. - * The base config received is a clone of the original KibanaFeatureConfig, so it can be mutated safely. - * The modifications are applied before applying the rest of the properties of the ProductFeatureConfigs. + * Function 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 to be mutated + * @param kibanaFeatureConfig to be mutated * @returns void */ - baseFeatureConfigModifier?: (baseFeatureConfig: BaseKibanaFeatureConfig) => void; + featureConfigModifier?: FeatureConfigModifier; }; -export type ProductFeaturesConfig = Map< + +export type ProductFeaturesConfig< + K extends ProductFeatureKeyType, + T extends string = string +> = Partial>>; + +export type ProductFeaturesConfigMap = Map< ProductFeatureKeyType, ProductFeatureKibanaConfig >; -export type ProductFeaturesSecurityConfig = Map< +export type SecurityProductFeaturesConfigMap = Map< ProductFeatureSecurityKey, ProductFeatureKibanaConfig >; -export type ProductFeaturesCasesConfig = Map< +export type CasesProductFeaturesConfigMap = Map< ProductFeatureCasesKey, ProductFeatureKibanaConfig >; -export type ProductFeaturesAssistantConfig = Map< +export type AssistantProductFeaturesConfigMap = Map< ProductFeatureAssistantKey, ProductFeatureKibanaConfig >; -export type ProductFeaturesAttackDiscoveryConfig = Map< +export type AttackDiscoveryProductFeaturesConfigMap = Map< ProductFeatureAttackDiscoveryKey, ProductFeatureKibanaConfig >; -export type ProductFeaturesTimelineConfig = Map< +export type TimelineProductFeaturesConfigMap = Map< ProductFeatureTimelineFeatureKey, ProductFeatureKibanaConfig >; -export type ProductFeaturesNotesConfig = Map< +export type NotesProductFeaturesConfigMap = Map< ProductFeatureNotesFeatureKey, ProductFeatureKibanaConfig >; -export type ProductFeaturesSiemMigrationsConfig = Map< +export type SiemMigrationsProductFeaturesConfigMap = Map< ProductFeatureSiemMigrationsKey, ProductFeatureKibanaConfig >; @@ -94,11 +110,11 @@ export interface ProductFeatureParams { } export interface ProductFeaturesConfigurator { - security: () => ProductFeaturesConfig; - cases: () => ProductFeaturesConfig; - securityAssistant: () => ProductFeaturesConfig; - attackDiscovery: () => ProductFeaturesConfig; - timeline: () => ProductFeaturesConfig; - notes: () => ProductFeaturesConfig; - siemMigrations: () => ProductFeaturesConfig; + security: () => SecurityProductFeaturesConfigMap; + cases: () => CasesProductFeaturesConfigMap; + securityAssistant: () => AssistantProductFeaturesConfigMap; + attackDiscovery: () => AttackDiscoveryProductFeaturesConfigMap; + timeline: () => TimelineProductFeaturesConfigMap; + notes: () => NotesProductFeaturesConfigMap; + siemMigrations: () => SiemMigrationsProductFeaturesConfigMap; } 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..a60dfd2edc71a 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,24 +12,18 @@ import type { FeaturesPluginSetup, } from '@kbn/features-plugin/server'; import type { - ProductFeaturesConfig, - AppSubFeaturesMap, - BaseKibanaFeatureConfig, + ProductFeaturesConfigMap, + ProductFeatureParams, } from '@kbn/security-solution-features'; import { ProductFeaturesConfigMerger } from './product_features_config_merger'; -export class ProductFeatures { +export class ProductFeatures { private featureConfigMerger: ProductFeaturesConfigMerger; private featuresSetup?: FeaturesPluginSetup; 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, private readonly params: ProductFeatureParams) { + this.featureConfigMerger = new ProductFeaturesConfigMerger(this.logger, params.subFeaturesMap); this.registeredActions = new Set(); } @@ -37,7 +31,7 @@ export class ProductFeatures) { + public setConfig(productFeatureConfig: ProductFeaturesConfigMap) { if (this.featuresSetup == null) { throw new Error( 'Cannot sync kibana features as featuresSetup is not present. Did you call init?' @@ -45,8 +39,8 @@ export class ProductFeatures { const enabledProductFeaturesConfigs: ProductFeatureKibanaConfig[] = [ { subFeatureIds: ['subFeature3', 'subFeature1'], - baseFeatureConfigModifier: jest - .fn() - .mockImplementation((baseConfig: KibanaFeatureConfig): KibanaFeatureConfig => { - return { ...baseConfig, name: 'NEW NAME' }; - }), + featureConfigModifier: jest.fn().mockImplementation((baseConfig: KibanaFeatureConfig) => { + baseConfig.name = 'NEW NAME'; + }), }, { - baseFeatureConfigModifier: jest - .fn() - .mockImplementation((baseConfig: KibanaFeatureConfig): KibanaFeatureConfig => { - return { ...baseConfig, order: 666 }; - }), + featureConfigModifier: jest.fn().mockImplementation((baseConfig: KibanaFeatureConfig) => { + baseConfig.order = 666; + }), }, ]; @@ -377,14 +373,6 @@ describe('ProductFeaturesConfigMerger', () => { enabledProductFeaturesConfigs ); - expect(enabledProductFeaturesConfigs[0].baseFeatureConfigModifier).toBeCalledWith( - baseKibanaFeature - ); - expect(enabledProductFeaturesConfigs[1].baseFeatureConfigModifier).toBeCalledWith({ - ...baseKibanaFeature, - name: 'NEW NAME', - }); - expect(merged).toEqual({ ...baseKibanaFeature, 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 851f7b3f707fb..40994b5e59840 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 @@ -12,6 +12,7 @@ import type { ProductFeatureKibanaConfig, BaseKibanaFeatureConfig, SubFeaturesPrivileges, + FeatureConfigModifier, } from '@kbn/security-solution-features'; export class ProductFeaturesConfigMerger { @@ -33,16 +34,18 @@ export class ProductFeaturesConfigMerger { productFeaturesConfigs: ProductFeatureKibanaConfig[] ): KibanaFeatureConfig { const mergedKibanaFeatureConfig = cloneDeep(kibanaFeatureConfig) as KibanaFeatureConfig; - const subFeaturesPrivilegesToMerge: SubFeaturesPrivileges[] = []; + const enabledSubFeaturesIndexed = Object.fromEntries( kibanaSubFeatureIds.map((id) => [id, true]) ); + const subFeaturesPrivilegesToMerge: SubFeaturesPrivileges[] = []; + const featureConfigModifiers: FeatureConfigModifier[] = []; productFeaturesConfigs.forEach((productFeatureConfig) => { const { subFeaturesPrivileges, subFeatureIds, - baseFeatureConfigModifier, + featureConfigModifier, ...productFeatureConfigToMerge } = cloneDeep(productFeatureConfig); @@ -54,10 +57,10 @@ export class ProductFeaturesConfigMerger { subFeaturesPrivilegesToMerge.push(...subFeaturesPrivileges); } - // apply custom modifications before merging - if (baseFeatureConfigModifier) { - baseFeatureConfigModifier(mergedKibanaFeatureConfig); + if (featureConfigModifier) { + featureConfigModifiers.push(featureConfigModifier); } + mergeWith(mergedKibanaFeatureConfig, productFeatureConfigToMerge, featureConfigMerger); }); @@ -76,6 +79,11 @@ export class ProductFeaturesConfigMerger { mergedKibanaFeatureConfig.subFeatures = mergedKibanaSubFeatures; + // Apply custom modifications after merging all the product feature configs, including the subFeatures + featureConfigModifiers.forEach((modifier) => { + modifier(mergedKibanaFeatureConfig); + }); + return mergedKibanaFeatureConfig; } 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..ae4478a50fb0b 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,7 +8,7 @@ import { ProductFeaturesService } from './product_features_service'; import { ProductFeatures } from './product_features'; import type { - ProductFeaturesConfig, + ProductFeaturesConfigMap, BaseKibanaFeatureConfig, ProductFeaturesConfigurator, } from '@kbn/security-solution-features'; @@ -92,13 +92,13 @@ describe('ProductFeaturesService', () => { 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 mockSecurityConfig = new Map() as ProductFeaturesConfigMap; + const mockCasesConfig = new Map() as ProductFeaturesConfigMap; + const mockAssistantConfig = new Map() as ProductFeaturesConfigMap; + const mockAttackDiscoveryConfig = new Map() as ProductFeaturesConfigMap; + const mockSiemMigrationsConfig = new Map() as ProductFeaturesConfigMap; + const mockTimelineConfig = new Map() as ProductFeaturesConfigMap; + const mockNotesConfig = new Map() as ProductFeaturesConfigMap; const configurator: ProductFeaturesConfigurator = { security: jest.fn(() => mockSecurityConfig), @@ -145,21 +145,21 @@ describe('ProductFeaturesService', () => { const mockSecurityConfig = new Map([ [ProductFeatureKey.advancedInsights, {}], [ProductFeatureKey.endpointExceptions, {}], - ]) as ProductFeaturesConfig; + ]) as ProductFeaturesConfigMap; const mockCasesConfig = new Map([ [ProductFeatureKey.casesConnectors, {}], - ]) as ProductFeaturesConfig; + ]) as ProductFeaturesConfigMap; const mockAssistantConfig = new Map([ [ProductFeatureKey.assistant, {}], - ]) as ProductFeaturesConfig; + ]) as ProductFeaturesConfigMap; const mockAttackDiscoveryConfig = new Map([ [ProductFeatureKey.attackDiscovery, {}], - ]) as ProductFeaturesConfig; + ]) as ProductFeaturesConfigMap; const mockSiemMigrationsConfig = new Map([ [ProductFeatureKey.siemMigrations, {}], - ]) as ProductFeaturesConfig; - const mockTimelineConfig = new Map([[ProductFeatureKey.timeline, {}]]) as ProductFeaturesConfig; - const mockNotesConfig = new Map([[ProductFeatureKey.notes, {}]]) as ProductFeaturesConfig; + ]) as ProductFeaturesConfigMap; + const mockTimelineConfig = new Map([[ProductFeatureKey.timeline, {}]]) as ProductFeaturesConfigMap; + const mockNotesConfig = new Map([[ProductFeatureKey.notes, {}]]) as ProductFeaturesConfigMap; const configurator: ProductFeaturesConfigurator = { security: jest.fn(() => mockSecurityConfig), 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..982406c2cf22c 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 @@ -58,120 +58,63 @@ export class ProductFeaturesService { ) { const securityFeature = getSecurityFeature({ savedObjects: securityV1SavedObjects, - experimentalFeatures: this.experimentalFeatures, + experimentalFeatures, }); - this.securityProductFeatures = new ProductFeatures( - this.logger, - securityFeature.subFeaturesMap, - securityFeature.baseKibanaFeature, - securityFeature.baseKibanaSubFeatureIds - ); + this.securityProductFeatures = new ProductFeatures(this.logger, securityFeature); + const securityV2Feature = getSecurityV2Feature({ savedObjects: securityDefaultSavedObjects, - experimentalFeatures: this.experimentalFeatures, + experimentalFeatures, }); - this.securityV2ProductFeatures = new ProductFeatures( - this.logger, - securityV2Feature.subFeaturesMap, - securityV2Feature.baseKibanaFeature, - securityV2Feature.baseKibanaSubFeatureIds - ); + this.securityV2ProductFeatures = new ProductFeatures(this.logger, securityV2Feature); const securityV3Feature = getSecurityV3Feature({ savedObjects: securityDefaultSavedObjects, - experimentalFeatures: this.experimentalFeatures, + experimentalFeatures, }); - this.securityV3ProductFeatures = new ProductFeatures( - this.logger, - securityV3Feature.subFeaturesMap, - securityV3Feature.baseKibanaFeature, - securityV3Feature.baseKibanaSubFeatureIds - ); + this.securityV3ProductFeatures = new ProductFeatures(this.logger, securityV3Feature); const casesFeature = getCasesFeature({ uiCapabilities: casesUiCapabilities, apiTags: casesApiTags, savedObjects: { files: filesSavedObjectTypes }, }); - - this.casesProductFeatures = new ProductFeatures( - this.logger, - casesFeature.subFeaturesMap, - casesFeature.baseKibanaFeature, - casesFeature.baseKibanaSubFeatureIds - ); + this.casesProductFeatures = new ProductFeatures(this.logger, casesFeature); const casesV2Feature = getCasesV2Feature({ uiCapabilities: casesUiCapabilities, apiTags: casesApiTags, savedObjects: { files: filesSavedObjectTypes }, }); - - this.casesProductV2Features = new ProductFeatures( - this.logger, - casesV2Feature.subFeaturesMap, - casesV2Feature.baseKibanaFeature, - casesV2Feature.baseKibanaSubFeatureIds - ); + this.casesProductV2Features = new ProductFeatures(this.logger, casesV2Feature); const casesV3Feature = getCasesV3Feature({ uiCapabilities: casesUiCapabilities, apiTags: casesApiTags, savedObjects: { files: filesSavedObjectTypes }, }); - this.casesProductFeaturesV3 = new ProductFeatures( - this.logger, - casesV3Feature.subFeaturesMap, - casesV3Feature.baseKibanaFeature, - casesV3Feature.baseKibanaSubFeatureIds - ); + this.casesProductFeaturesV3 = new ProductFeatures(this.logger, casesV3Feature); - const assistantFeature = getAssistantFeature(this.experimentalFeatures); - this.securityAssistantProductFeatures = new ProductFeatures( - this.logger, - assistantFeature.subFeaturesMap, - assistantFeature.baseKibanaFeature, - assistantFeature.baseKibanaSubFeatureIds - ); + const assistantFeature = getAssistantFeature(experimentalFeatures); + this.securityAssistantProductFeatures = new ProductFeatures(this.logger, assistantFeature); const attackDiscoveryFeature = getAttackDiscoveryFeature(); - - this.attackDiscoveryProductFeatures = new ProductFeatures( - this.logger, - attackDiscoveryFeature.subFeaturesMap, - attackDiscoveryFeature.baseKibanaFeature, - attackDiscoveryFeature.baseKibanaSubFeatureIds - ); + this.attackDiscoveryProductFeatures = new ProductFeatures(this.logger, attackDiscoveryFeature); const timelineFeature = getTimelineFeature({ savedObjects: securityTimelineSavedObjects, - experimentalFeatures: {}, + experimentalFeatures, }); - this.timelineProductFeatures = new ProductFeatures( - this.logger, - timelineFeature.subFeaturesMap, - timelineFeature.baseKibanaFeature, - timelineFeature.baseKibanaSubFeatureIds - ); + this.timelineProductFeatures = new ProductFeatures(this.logger, timelineFeature); const notesFeature = getNotesFeature({ savedObjects: securityNotesSavedObjects, - experimentalFeatures: {}, + experimentalFeatures, }); - this.notesProductFeatures = new ProductFeatures( - this.logger, - notesFeature.subFeaturesMap, - notesFeature.baseKibanaFeature, - notesFeature.baseKibanaSubFeatureIds - ); + this.notesProductFeatures = new ProductFeatures(this.logger, notesFeature); const siemMigrationsFeature = getSiemMigrationsFeature(); - this.siemMigrationsProductFeatures = new ProductFeatures( - this.logger, - siemMigrationsFeature.subFeaturesMap, - siemMigrationsFeature.baseKibanaFeature, - siemMigrationsFeature.baseKibanaSubFeatureIds - ); + this.siemMigrationsProductFeatures = new ProductFeatures(this.logger, siemMigrationsFeature); } public init(featuresSetup: FeaturesPluginSetup) { diff --git a/x-pack/solutions/security/plugins/security_solution_ess/common/constants.ts b/x-pack/solutions/security/plugins/security_solution_ess/common/constants.ts index 0d0bfcd80d70d..347b4aad4ce49 100644 --- a/x-pack/solutions/security/plugins/security_solution_ess/common/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution_ess/common/constants.ts @@ -5,18 +5,18 @@ * 2.0. */ -import type { ProductFeatureKeyType } from '@kbn/security-solution-features/keys'; +import type { ProductFeatureKey } from '@kbn/security-solution-features/keys'; import { ALL_PRODUCT_FEATURE_KEYS, ProductFeatureSecurityKey, } from '@kbn/security-solution-features/keys'; // List of product features that are disabled in different offering (eg. Serverless). -const DISABLED_PRODUCT_FEATURES: ProductFeatureKeyType[] = [ +const DISABLED_PRODUCT_FEATURES: ProductFeatureKey[] = [ ProductFeatureSecurityKey.externalDetections, ProductFeatureSecurityKey.configurations, ]; export const DEFAULT_PRODUCT_FEATURES = ALL_PRODUCT_FEATURE_KEYS.filter( - (key) => !DISABLED_PRODUCT_FEATURES.includes(key as ProductFeatureKeyType) + (key) => !DISABLED_PRODUCT_FEATURES.includes(key as ProductFeatureKey) ); 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 index 732128415a394..2d41a2054d705 100644 --- 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 @@ -7,7 +7,7 @@ import type { ProductFeatureKeys, ProductFeatureKibanaConfig, - ProductFeaturesAssistantConfig, + AssistantProductFeaturesConfigMap, } from '@kbn/security-solution-features'; import { assistantDefaultProductFeaturesConfig, @@ -19,7 +19,7 @@ import type { } from '@kbn/security-solution-features/keys'; export const getSecurityAssistantProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesAssistantConfig => { + (enabledProductFeatureKeys: ProductFeatureKeys) => (): AssistantProductFeaturesConfigMap => { return createEnabledProductFeaturesConfigMap( assistantProductFeaturesConfig, enabledProductFeatureKeys 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 index 9e575e805e203..c05e2287b6a5d 100644 --- 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 @@ -8,7 +8,7 @@ import type { ProductFeatureKeys, ProductFeatureKibanaConfig, - ProductFeaturesAttackDiscoveryConfig, + AttackDiscoveryProductFeaturesConfigMap, } from '@kbn/security-solution-features'; import { attackDiscoveryDefaultProductFeaturesConfig, @@ -34,7 +34,7 @@ const attackDiscoveryProductFeaturesConfig: Record< }; export const getAttackDiscoveryProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesAttackDiscoveryConfig => + (enabledProductFeatureKeys: ProductFeatureKeys) => (): AttackDiscoveryProductFeaturesConfigMap => 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 index cdf8d10742373..ba19ec7cc73d8 100644 --- 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 @@ -6,7 +6,7 @@ */ import type { ProductFeatureKibanaConfig, - ProductFeaturesCasesConfig, + CasesProductFeaturesConfigMap, ProductFeatureKeys, } from '@kbn/security-solution-features'; import type { @@ -24,7 +24,7 @@ import { } from '@kbn/cases-plugin/common/constants'; export const getCasesProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesCasesConfig => { + (enabledProductFeatureKeys: ProductFeatureKeys) => (): CasesProductFeaturesConfigMap => { return createEnabledProductFeaturesConfigMap( casesProductFeaturesConfig, enabledProductFeatureKeys 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 index 0b9f1e143044a..f2cde2d6b2fdf 100644 --- 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 @@ -7,7 +7,7 @@ import type { ProductFeatureKeys, ProductFeatureKibanaConfig, - ProductFeaturesNotesConfig, + NotesProductFeaturesConfigMap, } from '@kbn/security-solution-features'; import { notesDefaultProductFeaturesConfig, @@ -33,5 +33,5 @@ const notesProductFeaturesConfig: Record< }; export const getNotesProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesNotesConfig => + (enabledProductFeatureKeys: ProductFeatureKeys) => (): NotesProductFeaturesConfigMap => createEnabledProductFeaturesConfigMap(notesProductFeaturesConfig, enabledProductFeatureKeys); 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 index 200ff377c6596..c028e227aa929 100644 --- 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 @@ -6,8 +6,7 @@ */ import type { ProductFeatureKeys, - ProductFeatureKibanaConfig, - ProductFeaturesSecurityConfig, + SecurityProductFeaturesConfigMap, } from '@kbn/security-solution-features'; import { ProductFeatureSecurityKey, @@ -19,29 +18,19 @@ import { } 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'; +import type { SecurityProductFeaturesConfig } from '@kbn/security-solution-features/src/security/types'; export const getSecurityProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesSecurityConfig => { + (enabledProductFeatureKeys: ProductFeatureKeys) => (): SecurityProductFeaturesConfigMap => { return createEnabledProductFeaturesConfigMap( - securityProductFeaturesConfig, + securityEssProductFeaturesConfig, 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 -> = { +const securityEssProductFeaturesConfig: SecurityProductFeaturesConfig = { ...securityDefaultProductFeaturesConfig, + [ProductFeatureSecurityKey.endpointExceptions]: { privileges: { all: { @@ -70,7 +59,7 @@ const securityProductFeaturesConfig: Record< // When endpointArtifactManagement PLI is enabled, the replacedBy for the siemV3 feature needs to // account for the privileges of the sub-features that are introduced by it. - baseFeatureConfigModifier: (baseFeatureConfig) => { + featureConfigModifier: (baseFeatureConfig) => { const replacedBy = baseFeatureConfig.privileges?.all?.replacedBy; if (!replacedBy) { return; 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 index 6e42f7009cdbf..4e1339048e5c8 100644 --- 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 @@ -8,7 +8,7 @@ import type { ProductFeatureKeys, ProductFeatureKibanaConfig, - ProductFeaturesSiemMigrationsConfig, + SiemMigrationsProductFeaturesConfigMap, } from '@kbn/security-solution-features'; import { siemMigrationsDefaultProductFeaturesConfig, @@ -34,7 +34,7 @@ const siemMigrationsProductFeaturesConfig: Record< }; export const getSiemMigrationsProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesSiemMigrationsConfig => + (enabledProductFeatureKeys: ProductFeatureKeys) => (): SiemMigrationsProductFeaturesConfigMap => 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 index 411c06ecfe75f..26fe891246abc 100644 --- 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 @@ -8,7 +8,7 @@ import type { ProductFeatureKeys, ProductFeatureKibanaConfig, - ProductFeaturesTimelineConfig, + TimelineProductFeaturesConfigMap, } from '@kbn/security-solution-features'; import { createEnabledProductFeaturesConfigMap, @@ -34,5 +34,5 @@ const timelineProductFeaturesConfig: Record< }; export const getTimelineProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesTimelineConfig => + (enabledProductFeatureKeys: ProductFeatureKeys) => (): TimelineProductFeaturesConfigMap => createEnabledProductFeaturesConfigMap(timelineProductFeaturesConfig, enabledProductFeatureKeys); diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/pages/endpoint_management/endpoint_exceptions_details.tsx b/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/pages/endpoint_management/endpoint_exceptions_details.tsx index 99313d79783c3..e6bf202c7bffd 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/pages/endpoint_management/endpoint_exceptions_details.tsx +++ b/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/pages/endpoint_management/endpoint_exceptions_details.tsx @@ -8,10 +8,10 @@ import { EuiEmptyPrompt, EuiIcon } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import React, { memo } from 'react'; -import type { ProductFeatureKeyType } from '@kbn/security-solution-features/keys'; +import type { ProductFeatureKey } from '@kbn/security-solution-features/keys'; import { getProductTypeByPLI } from '../../hooks/use_product_type_by_pli'; -const EndpointExceptionsDetailsUpselling: React.FC<{ requiredPLI: ProductFeatureKeyType }> = memo( +const EndpointExceptionsDetailsUpselling: React.FC<{ requiredPLI: ProductFeatureKey }> = memo( ({ requiredPLI }) => { const productTypeRequired = getProductTypeByPLI(requiredPLI); 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 index 27feabee5c0fd..3cc06c0705aca 100644 --- 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 @@ -7,7 +7,7 @@ import type { ProductFeatureKeys, ProductFeatureKibanaConfig, - ProductFeaturesAssistantConfig, + AssistantProductFeaturesConfigMap, } from '@kbn/security-solution-features'; import { assistantDefaultProductFeaturesConfig, @@ -19,7 +19,7 @@ import type { } from '@kbn/security-solution-features/keys'; export const getSecurityAssistantProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesAssistantConfig => { + (enabledProductFeatureKeys: ProductFeatureKeys) => (): AssistantProductFeaturesConfigMap => { return createEnabledProductFeaturesConfigMap( assistantProductFeaturesConfig, enabledProductFeatureKeys 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 index 406c396edfb72..7ca34215a5594 100644 --- 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 @@ -7,7 +7,7 @@ import type { ProductFeatureKeys, ProductFeatureKibanaConfig, - ProductFeaturesAttackDiscoveryConfig, + AttackDiscoveryProductFeaturesConfigMap, } from '@kbn/security-solution-features'; import { attackDiscoveryDefaultProductFeaturesConfig, @@ -33,7 +33,7 @@ const attackDiscoveryProductFeaturesConfig: Record< }; export const getAttackDiscoveryProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesAttackDiscoveryConfig => + (enabledProductFeatureKeys: ProductFeatureKeys) => (): AttackDiscoveryProductFeaturesConfigMap => 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 index 21c1fa88e56cb..834bae961a767 100644 --- 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 @@ -6,7 +6,7 @@ */ import type { ProductFeatureKibanaConfig, - ProductFeaturesCasesConfig, + CasesProductFeaturesConfigMap, ProductFeatureKeys, } from '@kbn/security-solution-features'; import { @@ -23,7 +23,7 @@ import { } from '@kbn/cases-plugin/common/constants'; export const getCasesProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesCasesConfig => { + (enabledProductFeatureKeys: ProductFeatureKeys) => (): CasesProductFeaturesConfigMap => { return createEnabledProductFeaturesConfigMap( casesProductFeaturesConfig, enabledProductFeatureKeys 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 index 1b00761f6da4a..ca34d543cefca 100644 --- 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 @@ -7,7 +7,7 @@ import type { ProductFeatureKeys, ProductFeatureKibanaConfig, - ProductFeaturesNotesConfig, + NotesProductFeaturesConfigMap, } from '@kbn/security-solution-features'; import { notesDefaultProductFeaturesConfig, @@ -33,5 +33,5 @@ const notesProductFeaturesConfig: Record< }; export const getNotesProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesNotesConfig => + (enabledProductFeatureKeys: ProductFeatureKeys) => (): NotesProductFeaturesConfigMap => createEnabledProductFeaturesConfigMap(notesProductFeaturesConfig, enabledProductFeatureKeys); 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 index 61d61772dc613..72a2d369d0728 100644 --- 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 @@ -6,8 +6,7 @@ */ import type { ProductFeatureKeys, - ProductFeatureKibanaConfig, - ProductFeaturesSecurityConfig, + SecurityProductFeaturesConfigMap, } from '@kbn/security-solution-features'; import { securityDefaultProductFeaturesConfig, @@ -19,6 +18,7 @@ import { } 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 { SecurityProductFeaturesConfig } from '@kbn/security-solution-features/src/security/types'; import type { ExperimentalFeatures } from '../../common/experimental_features'; export const getSecurityProductFeaturesConfigurator = @@ -26,27 +26,16 @@ export const getSecurityProductFeaturesConfigurator = enabledProductFeatureKeys: ProductFeatureKeys, _: ExperimentalFeatures // currently un-used, but left here as a convenience for possible future use ) => - (): ProductFeaturesSecurityConfig => { + (): SecurityProductFeaturesConfigMap => { 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 -> = { +const securityProductFeaturesConfig: SecurityProductFeaturesConfig = { ...securityDefaultProductFeaturesConfig, + [ProductFeatureSecurityKey.endpointExceptions]: { subFeatureIds: [SecuritySubFeatureId.endpointExceptions], }, @@ -66,7 +55,7 @@ const securityProductFeaturesConfig: Record< // When endpointArtifactManagement PLI is enabled, the replacedBy for the siemV3 feature needs to // account for the privileges of the sub-features that are introduced by it. - baseFeatureConfigModifier: (baseFeatureConfig) => { + featureConfigModifier: (baseFeatureConfig) => { const replacedBy = baseFeatureConfig.privileges?.all?.replacedBy; if (!replacedBy || !('default' in replacedBy)) { return; 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 index b6bcb93c8f8ad..7717b8e735ff1 100644 --- 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 @@ -7,7 +7,7 @@ import type { ProductFeatureKeys, ProductFeatureKibanaConfig, - ProductFeaturesSiemMigrationsConfig, + SiemMigrationsProductFeaturesConfigMap, } from '@kbn/security-solution-features'; import { siemMigrationsDefaultProductFeaturesConfig, @@ -33,7 +33,7 @@ const siemMigrationsProductFeaturesConfig: Record< }; export const getSiemMigrationsProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesSiemMigrationsConfig => + (enabledProductFeatureKeys: ProductFeatureKeys) => (): SiemMigrationsProductFeaturesConfigMap => 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 index fde220b83cb30..0f98f7bbba09c 100644 --- 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 @@ -7,7 +7,7 @@ import type { ProductFeatureKeys, ProductFeatureKibanaConfig, - ProductFeaturesTimelineConfig, + TimelineProductFeaturesConfigMap, } from '@kbn/security-solution-features'; import { timelineDefaultProductFeaturesConfig, @@ -33,5 +33,5 @@ const timelineProductFeaturesConfig: Record< }; export const getTimelineProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesTimelineConfig => + (enabledProductFeatureKeys: ProductFeatureKeys) => (): TimelineProductFeaturesConfigMap => createEnabledProductFeaturesConfigMap(timelineProductFeaturesConfig, enabledProductFeatureKeys); From 3b82104b2afe7f6a14fee94f96a18d4c0cd6bd6b Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Thu, 24 Jul 2025 13:58:57 +0200 Subject: [PATCH 04/28] fix types --- .../plugins/security_solution_ess/common/constants.ts | 6 +++--- .../endpoint_management/endpoint_exceptions_details.tsx | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution_ess/common/constants.ts b/x-pack/solutions/security/plugins/security_solution_ess/common/constants.ts index 347b4aad4ce49..0d0bfcd80d70d 100644 --- a/x-pack/solutions/security/plugins/security_solution_ess/common/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution_ess/common/constants.ts @@ -5,18 +5,18 @@ * 2.0. */ -import type { ProductFeatureKey } from '@kbn/security-solution-features/keys'; +import type { ProductFeatureKeyType } from '@kbn/security-solution-features/keys'; import { ALL_PRODUCT_FEATURE_KEYS, ProductFeatureSecurityKey, } from '@kbn/security-solution-features/keys'; // List of product features that are disabled in different offering (eg. Serverless). -const DISABLED_PRODUCT_FEATURES: ProductFeatureKey[] = [ +const DISABLED_PRODUCT_FEATURES: ProductFeatureKeyType[] = [ ProductFeatureSecurityKey.externalDetections, ProductFeatureSecurityKey.configurations, ]; export const DEFAULT_PRODUCT_FEATURES = ALL_PRODUCT_FEATURE_KEYS.filter( - (key) => !DISABLED_PRODUCT_FEATURES.includes(key as ProductFeatureKey) + (key) => !DISABLED_PRODUCT_FEATURES.includes(key as ProductFeatureKeyType) ); diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/pages/endpoint_management/endpoint_exceptions_details.tsx b/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/pages/endpoint_management/endpoint_exceptions_details.tsx index e6bf202c7bffd..99313d79783c3 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/pages/endpoint_management/endpoint_exceptions_details.tsx +++ b/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/pages/endpoint_management/endpoint_exceptions_details.tsx @@ -8,10 +8,10 @@ import { EuiEmptyPrompt, EuiIcon } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import React, { memo } from 'react'; -import type { ProductFeatureKey } from '@kbn/security-solution-features/keys'; +import type { ProductFeatureKeyType } from '@kbn/security-solution-features/keys'; import { getProductTypeByPLI } from '../../hooks/use_product_type_by_pli'; -const EndpointExceptionsDetailsUpselling: React.FC<{ requiredPLI: ProductFeatureKey }> = memo( +const EndpointExceptionsDetailsUpselling: React.FC<{ requiredPLI: ProductFeatureKeyType }> = memo( ({ requiredPLI }) => { const productTypeRequired = getProductTypeByPLI(requiredPLI); From 03c37389bd0253786f819b229f12bf59ba49dcc8 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 24 Jul 2025 13:07:28 +0000 Subject: [PATCH 05/28] [CI] Auto-commit changed files from 'node scripts/eslint_all_files --no-cache --fix' --- .../product_features_service/product_features_service.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 ae4478a50fb0b..12f85d792d1e8 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 @@ -158,7 +158,9 @@ describe('ProductFeaturesService', () => { const mockSiemMigrationsConfig = new Map([ [ProductFeatureKey.siemMigrations, {}], ]) as ProductFeaturesConfigMap; - const mockTimelineConfig = new Map([[ProductFeatureKey.timeline, {}]]) as ProductFeaturesConfigMap; + const mockTimelineConfig = new Map([ + [ProductFeatureKey.timeline, {}], + ]) as ProductFeaturesConfigMap; const mockNotesConfig = new Map([[ProductFeatureKey.notes, {}]]) as ProductFeaturesConfigMap; const configurator: ProductFeaturesConfigurator = { From d42f7050292f53cc3555780eae52f438baab3849 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Fri, 25 Jul 2025 15:28:33 +0200 Subject: [PATCH 06/28] service complexity reduction --- .../security/packages/features/src/types.ts | 2 + ...ges.ts => cases_product_feature_params.ts} | 11 +- .../lib/product_features_service/mocks.ts | 6 +- .../product_features.test.ts | 146 ++++----- .../product_features.ts | 51 +++- .../product_features_api_access_control.ts | 101 +++++++ .../product_features_service.test.ts | 123 ++++---- .../product_features_service.ts | 286 ++++-------------- .../security_solution/server/plugin.ts | 3 +- 9 files changed, 317 insertions(+), 412 deletions(-) rename x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/{cases_privileges.ts => cases_product_feature_params.ts} (79%) create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_api_access_control.ts diff --git a/x-pack/solutions/security/packages/features/src/types.ts b/x-pack/solutions/security/packages/features/src/types.ts index 953373e6f2bcf..305c64921be80 100644 --- a/x-pack/solutions/security/packages/features/src/types.ts +++ b/x-pack/solutions/security/packages/features/src/types.ts @@ -118,3 +118,5 @@ export interface ProductFeaturesConfigurator { notes: () => NotesProductFeaturesConfigMap; siemMigrations: () => SiemMigrationsProductFeaturesConfigMap; } + +export type ProductFeatureGroup = keyof ProductFeaturesConfigurator; 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 79% 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..1e327d5c27266 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 { APP_ID } from '../../../common/constants'; const originalCasesUiCapabilities = createCasesUICapabilities(); const originalCasesApiTags = getCasesApiTags(APP_ID); -export const casesUiCapabilities = { +const casesUiCapabilities = { ...originalCasesUiCapabilities, all: originalCasesUiCapabilities.all.filter( (capability) => capability !== CASES_CONNECTORS_CAPABILITY @@ -29,7 +30,7 @@ export const casesUiCapabilities = { ), }; -export const casesApiTags = { +const casesApiTags = { ...originalCasesApiTags, all: originalCasesApiTags.all.filter( (capability) => capability !== GET_CONNECTORS_CONFIGURE_API_TAG @@ -38,3 +39,9 @@ export const casesApiTags = { (capability) => capability !== GET_CONNECTORS_CONFIGURE_API_TAG ), }; + +export const casesProductFeatureParams = { + uiCapabilities: casesUiCapabilities, + apiTags: casesApiTags, + 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..0cc42c610bf27 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(() => ({ @@ -82,7 +84,9 @@ export const createProductFeaturesServiceMock = ( ) => { const productFeaturesService = new ProductFeaturesService(logger, experimentalFeatures); - productFeaturesService.init(featuresPluginSetupContract); + productFeaturesService.setup(coreLifecycleMock.createCoreSetup(), { + features: featuresPluginSetupContract, + } as SecuritySolutionPluginSetupDependencies); if (enabledFeatureKeys) { productFeaturesService.setProductFeaturesConfigurator({ 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..18890e65cfbfe 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'; @@ -144,22 +146,27 @@ const expectedBaseWithTestConfigPrivileges = { }, }; +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, - [] - ); + const productFeatures = new ProductFeatures(logger); + + productFeatures.create(featureGroup, [testFeatureParams]); productFeatures.init(featuresSetup); - productFeatures.setConfig(new Map()); + productFeatures.register(featureGroup, new Map()); expect(featuresSetup.registerKibanaFeature).toHaveBeenCalledWith({ ...baseKibanaFeature, @@ -168,19 +175,12 @@ 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); + + productFeatures.create(featureGroup, [testFeatureParams]); productFeatures.init(featuresSetup); - productFeatures.setConfig( + productFeatures.register( + featureGroup, new Map([['test-feature' as ProductFeatureKeyType, testFeaturePrivilegeConfig]]) ); @@ -192,19 +192,12 @@ 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); + + productFeatures.create(featureGroup, [testFeatureParams]); productFeatures.init(featuresSetup); - productFeatures.setConfig( + productFeatures.register( + featureGroup, new Map([ ['test-feature' as ProductFeatureKeyType, testFeaturePrivilegeConfig], ['test-sub-feature' as ProductFeatureKeyType, testSubFeaturePrivilegeConfig], @@ -219,19 +212,14 @@ 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); + + productFeatures.create(featureGroup, [ + { ...testFeatureParams, baseKibanaSubFeatureIds: [SUB_FEATURE_2.name] }, + ]); productFeatures.init(featuresSetup); - productFeatures.setConfig( + productFeatures.register( + featureGroup, new Map([ ['test-feature' as ProductFeatureKeyType, testFeaturePrivilegeConfig], ['test-sub-feature' as ProductFeatureKeyType, testSubFeaturePrivilegeConfig], @@ -248,18 +236,10 @@ describe('ProductFeatures', () => { 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(featureGroup, new Map()); expect(productFeatures.isActionRegistered('api:api-read')).toEqual(true); expect(productFeatures.isActionRegistered('ui:read')).toEqual(true); @@ -274,18 +254,11 @@ 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); + productFeatures.create(featureGroup, [testFeatureParams]); productFeatures.init(featuresSetup); - productFeatures.setConfig( + productFeatures.register( + featureGroup, new Map([['test-feature' as ProductFeatureKeyType, testFeaturePrivilegeConfig]]) ); @@ -302,18 +275,12 @@ 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); + + productFeatures.create(featureGroup, [testFeatureParams]); productFeatures.init(featuresSetup); - productFeatures.setConfig( + productFeatures.register( + featureGroup, new Map([ ['test-feature' as ProductFeatureKeyType, testFeaturePrivilegeConfig], ['test-sub-feature' as ProductFeatureKeyType, testSubFeaturePrivilegeConfig], @@ -333,18 +300,13 @@ 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); + productFeatures.create(featureGroup, [ + { ...testFeatureParams, baseKibanaSubFeatureIds: [SUB_FEATURE_2.name] }, + ]); productFeatures.init(featuresSetup); - productFeatures.setConfig( + productFeatures.register( + featureGroup, new Map([ ['test-feature' as ProductFeatureKeyType, testFeaturePrivilegeConfig], ['test-sub-feature' as ProductFeatureKeyType, testSubFeaturePrivilegeConfig], 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 a60dfd2edc71a..99fab544a5b15 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 @@ -14,39 +14,58 @@ import type { import type { ProductFeaturesConfigMap, ProductFeatureParams, + ProductFeatureGroup, } from '@kbn/security-solution-features'; import { ProductFeaturesConfigMerger } from './product_features_config_merger'; -export class ProductFeatures { - private featureConfigMerger: ProductFeaturesConfigMerger; +export class ProductFeatures { private featuresSetup?: FeaturesPluginSetup; + private readonly productFeatures: Map; private readonly registeredActions: Set; - constructor(private readonly logger: Logger, private readonly params: ProductFeatureParams) { - this.featureConfigMerger = new ProductFeaturesConfigMerger(this.logger, params.subFeaturesMap); + constructor(private readonly logger: Logger) { + this.productFeatures = new Map(); this.registeredActions = new Set(); } + public create( + featureGroup: ProductFeatureGroup, + params: Array> + ) { + this.productFeatures.set(featureGroup, params); + } + public init(featuresSetup: FeaturesPluginSetup) { this.featuresSetup = featuresSetup; } - public setConfig(productFeatureConfig: ProductFeaturesConfigMap) { + public register( + featureGroup: ProductFeatureGroup, + productFeatureConfig: ProductFeaturesConfigMap + ) { 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 productFeaturesGroup = this.productFeatures.get(featureGroup); + if (!productFeaturesGroup) { + throw new Error(`No product feature found for group: ${featureGroup}`); } - const completeProductFeatureConfig = this.featureConfigMerger.mergeProductFeatureConfigs( - this.params.baseKibanaFeature, - this.params.baseKibanaSubFeatureIds, - Array.from(productFeatureConfig.values()) - ); + for (const params of productFeaturesGroup) { + const { baseKibanaFeature, baseKibanaSubFeatureIds, subFeaturesMap } = params; - this.logger.debug(() => JSON.stringify(completeProductFeatureConfig)); - this.featuresSetup.registerKibanaFeature(completeProductFeatureConfig); - this.addRegisteredActions(completeProductFeatureConfig); + const featureConfigMerger = new ProductFeaturesConfigMerger(this.logger, subFeaturesMap); + + const completeProductFeatureConfig = featureConfigMerger.mergeProductFeatureConfigs( + baseKibanaFeature, + baseKibanaSubFeatureIds, + Array.from(productFeatureConfig.values()) + ); + + this.logger.debug(() => JSON.stringify(completeProductFeatureConfig)); + 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_service.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_service.test.ts index ae4478a50fb0b..499df389bb7c2 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,19 @@ import { ProductFeaturesService } from './product_features_service'; import { ProductFeatures } from './product_features'; import type { - ProductFeaturesConfigMap, BaseKibanaFeatureConfig, ProductFeaturesConfigurator, + AssistantProductFeaturesConfigMap, + AttackDiscoveryProductFeaturesConfigMap, + CasesProductFeaturesConfigMap, + NotesProductFeaturesConfigMap, + SecurityProductFeaturesConfigMap, + SiemMigrationsProductFeaturesConfigMap, + TimelineProductFeaturesConfigMap, } 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 +29,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 +57,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 +72,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,12 +83,8 @@ 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', () => { @@ -89,16 +94,15 @@ describe('ProductFeaturesService', () => { experimentalFeatures ); - const featuresSetup = featuresPluginMock.createSetup(); - productFeaturesService.init(featuresSetup); + productFeaturesService.setup(coreSetup, pluginsSetup); - const mockSecurityConfig = new Map() as ProductFeaturesConfigMap; - const mockCasesConfig = new Map() as ProductFeaturesConfigMap; - const mockAssistantConfig = new Map() as ProductFeaturesConfigMap; - const mockAttackDiscoveryConfig = new Map() as ProductFeaturesConfigMap; - const mockSiemMigrationsConfig = new Map() as ProductFeaturesConfigMap; - const mockTimelineConfig = new Map() as ProductFeaturesConfigMap; - const mockNotesConfig = new Map() as ProductFeaturesConfigMap; + const mockSecurityConfig = new Map() as SecurityProductFeaturesConfigMap; + const mockCasesConfig = new Map() as CasesProductFeaturesConfigMap; + const mockAssistantConfig = new Map() as AssistantProductFeaturesConfigMap; + const mockAttackDiscoveryConfig = new Map() as AttackDiscoveryProductFeaturesConfigMap; + const mockSiemMigrationsConfig = new Map() as SiemMigrationsProductFeaturesConfigMap; + const mockTimelineConfig = new Map() as TimelineProductFeaturesConfigMap; + const mockNotesConfig = new Map() as NotesProductFeaturesConfigMap; const configurator: ProductFeaturesConfigurator = { security: jest.fn(() => mockSecurityConfig), @@ -117,19 +121,14 @@ describe('ProductFeaturesService', () => { 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 - ); + const { register } = MockedProductFeatures.mock.instances[0]; + expect(register).toHaveBeenCalledWith('security', mockSecurityConfig); + expect(register).toHaveBeenCalledWith('cases', mockCasesConfig); + expect(register).toHaveBeenCalledWith('securityAssistant', mockAssistantConfig); + expect(register).toHaveBeenCalledWith('attackDiscovery', mockAttackDiscoveryConfig); + expect(register).toHaveBeenCalledWith('siemMigrations', mockSiemMigrationsConfig); + expect(register).toHaveBeenCalledWith('timeline', mockTimelineConfig); + expect(register).toHaveBeenCalledWith('notes', mockNotesConfig); }); it('should return isEnabled for enabled features', () => { @@ -139,27 +138,30 @@ describe('ProductFeaturesService', () => { experimentalFeatures ); - const featuresSetup = featuresPluginMock.createSetup(); - productFeaturesService.init(featuresSetup); + productFeaturesService.setup(coreSetup, pluginsSetup); const mockSecurityConfig = new Map([ [ProductFeatureKey.advancedInsights, {}], [ProductFeatureKey.endpointExceptions, {}], - ]) as ProductFeaturesConfigMap; + ]) as SecurityProductFeaturesConfigMap; const mockCasesConfig = new Map([ [ProductFeatureKey.casesConnectors, {}], - ]) as ProductFeaturesConfigMap; + ]) as CasesProductFeaturesConfigMap; const mockAssistantConfig = new Map([ [ProductFeatureKey.assistant, {}], - ]) as ProductFeaturesConfigMap; + ]) as AssistantProductFeaturesConfigMap; const mockAttackDiscoveryConfig = new Map([ [ProductFeatureKey.attackDiscovery, {}], - ]) as ProductFeaturesConfigMap; + ]) as AttackDiscoveryProductFeaturesConfigMap; const mockSiemMigrationsConfig = new Map([ [ProductFeatureKey.siemMigrations, {}], - ]) as ProductFeaturesConfigMap; - const mockTimelineConfig = new Map([[ProductFeatureKey.timeline, {}]]) as ProductFeaturesConfigMap; - const mockNotesConfig = new Map([[ProductFeatureKey.notes, {}]]) as ProductFeaturesConfigMap; + ]) as SiemMigrationsProductFeaturesConfigMap; + const mockTimelineConfig = new Map([ + [ProductFeatureKey.timeline, {}], + ]) as TimelineProductFeaturesConfigMap; + const mockNotesConfig = new Map([ + [ProductFeatureKey.notes, {}], + ]) as NotesProductFeaturesConfigMap; const configurator: ProductFeaturesConfigurator = { security: jest.fn(() => mockSecurityConfig), @@ -181,30 +183,9 @@ describe('ProductFeaturesService', () => { 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 - ); - - productFeaturesService.isApiPrivilegeEnabled('writeEndpointExceptions'); - - 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('registerApiAccessControl', () => { - const mockHttpSetup = httpServiceMock.createSetupContract(); let lastRegisteredFn: OnPostAuthHandler; - mockHttpSetup.registerOnPostAuth.mockImplementation((fn) => { + coreSetup.http.registerOnPostAuth.mockImplementation((fn) => { lastRegisteredFn = fn; }); @@ -221,9 +202,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 +220,7 @@ describe('ProductFeaturesService', () => { loggerMock.create(), experimentalFeatures ); - productFeaturesService.registerApiAccessControl(mockHttpSetup); + productFeaturesService.setup(coreSetup, pluginsSetup); productFeaturesService.isEnabled = jest.fn().mockReturnValueOnce(false); @@ -256,7 +237,7 @@ describe('ProductFeaturesService', () => { loggerMock.create(), experimentalFeatures ); - productFeaturesService.registerApiAccessControl(mockHttpSetup); + productFeaturesService.setup(coreSetup, pluginsSetup); productFeaturesService.isEnabled = jest.fn().mockReturnValueOnce(true); @@ -279,7 +260,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 982406c2cf22c..0cc1311740bfd 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,10 +5,9 @@ * 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 { + ProductFeatureGroup, ProductFeatureKeyType, ProductFeaturesConfigurator, } from '@kbn/security-solution-features'; @@ -26,258 +25,89 @@ 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.securityProductFeatures = new ProductFeatures(this.logger, securityFeature); - - const securityV2Feature = getSecurityV2Feature({ - savedObjects: securityDefaultSavedObjects, - experimentalFeatures, - }); - this.securityV2ProductFeatures = new ProductFeatures(this.logger, securityV2Feature); - - const securityV3Feature = getSecurityV3Feature({ - savedObjects: securityDefaultSavedObjects, - experimentalFeatures, - }); - this.securityV3ProductFeatures = new ProductFeatures(this.logger, securityV3Feature); - - const casesFeature = getCasesFeature({ - uiCapabilities: casesUiCapabilities, - apiTags: casesApiTags, - savedObjects: { files: filesSavedObjectTypes }, - }); - this.casesProductFeatures = new ProductFeatures(this.logger, casesFeature); - - const casesV2Feature = getCasesV2Feature({ - uiCapabilities: casesUiCapabilities, - apiTags: casesApiTags, - savedObjects: { files: filesSavedObjectTypes }, - }); - this.casesProductV2Features = new ProductFeatures(this.logger, casesV2Feature); - - const casesV3Feature = getCasesV3Feature({ - uiCapabilities: casesUiCapabilities, - apiTags: casesApiTags, - savedObjects: { files: filesSavedObjectTypes }, - }); - this.casesProductFeaturesV3 = new ProductFeatures(this.logger, casesV3Feature); - - const assistantFeature = getAssistantFeature(experimentalFeatures); - this.securityAssistantProductFeatures = new ProductFeatures(this.logger, assistantFeature); - - const attackDiscoveryFeature = getAttackDiscoveryFeature(); - this.attackDiscoveryProductFeatures = new ProductFeatures(this.logger, attackDiscoveryFeature); - - const timelineFeature = getTimelineFeature({ - savedObjects: securityTimelineSavedObjects, - experimentalFeatures, - }); - this.timelineProductFeatures = new ProductFeatures(this.logger, timelineFeature); - - const notesFeature = getNotesFeature({ - savedObjects: securityNotesSavedObjects, - experimentalFeatures, - }); - this.notesProductFeatures = new ProductFeatures(this.logger, notesFeature); - - const siemMigrationsFeature = getSiemMigrationsFeature(); - this.siemMigrationsProductFeatures = new ProductFeatures(this.logger, siemMigrationsFeature); + public readonly logger: Logger; + private productFeaturesRegistry: ProductFeatures; + private enabledProductFeatures?: Set; + + constructor(loggerFactory: Logger, private readonly 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(this.experimentalFeatures), + ]); + this.productFeaturesRegistry.create('attackDiscovery', [getAttackDiscoveryFeature()]); + this.productFeaturesRegistry.create('timeline', [ + getTimelineFeature({ ...securityFeatureParams, savedObjects: securityTimelineSavedObjects }), + ]); + this.productFeaturesRegistry.create('notes', [ + getNotesFeature({ ...securityFeatureParams, savedObjects: securityNotesSavedObjects }), + ]); + 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 enabledProductFeatures: ProductFeatureKeyType[] = []; - 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 notesProductFeaturesConfig = configurator.notes(); - this.notesProductFeatures.setConfig(notesProductFeaturesConfig); - - let siemMigrationsProductFeaturesConfig = new Map(); - if (!this.experimentalFeatures.siemMigrationsDisabled) { - siemMigrationsProductFeaturesConfig = configurator.siemMigrations(); - this.siemMigrationsProductFeatures.setConfig(siemMigrationsProductFeaturesConfig); + for (const featureGroup of Object.keys(configurator) as ProductFeatureGroup[]) { + const productFeatureConfig = configurator[featureGroup](); + this.productFeaturesRegistry.register(featureGroup, productFeatureConfig); + enabledProductFeatures.push(...productFeatureConfig.keys()); } - 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(enabledProductFeatures); } + /** 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 fdc868d436794..8d1fa8e455e8f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts @@ -228,7 +228,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); @@ -306,7 +306,6 @@ export class Plugin implements ISecuritySolutionPlugin { productFeaturesService, }); - productFeaturesService.registerApiAccessControl(core.http); const router = core.http.createRouter(); core.http.registerRouteHandlerContext( APP_ID, From b7b9776cc72a33f6faa2cf081e330018a03c76d4 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 25 Jul 2025 15:26:58 +0000 Subject: [PATCH 07/28] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- .../solutions/security/plugins/security_solution/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/solutions/security/plugins/security_solution/tsconfig.json b/x-pack/solutions/security/plugins/security_solution/tsconfig.json index ed945d9637f39..edc26a8655def 100644 --- a/x-pack/solutions/security/plugins/security_solution/tsconfig.json +++ b/x-pack/solutions/security/plugins/security_solution/tsconfig.json @@ -256,5 +256,6 @@ "@kbn/elastic-assistant-shared-state", "@kbn/elastic-assistant-shared-state-plugin", "@kbn/spaces-utils", + "@kbn/core-lifecycle-server-mocks", ] } From 14def6c0dbb06757d204ed49f6a23058ba86bc57 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Fri, 25 Jul 2025 18:29:05 +0200 Subject: [PATCH 08/28] simplify offering product features configs --- .../assistant_product_features_config.ts | 24 +-------------- ...ttack_discovery_product_features_config.ts | 21 +------------ .../cases_product_features_config.ts | 30 ++++--------------- .../notes_product_features_config.ts | 24 +++------------ ...siem_migrations_product_features_config.ts | 21 +------------ .../timeline_product_features_config.ts | 24 +++------------ .../assistant_product_features_config.ts | 24 +-------------- ...ttack_discovery_product_features_config.ts | 21 +------------ .../cases_product_features_config.ts | 30 ++++--------------- .../notes_product_features_config.ts | 24 +++------------ ...siem_migrations_product_features_config.ts | 21 +------------ .../timeline_product_features_config.ts | 24 +++------------ 12 files changed, 32 insertions(+), 256 deletions(-) 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 index 2d41a2054d705..d101537a0812d 100644 --- 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 @@ -6,39 +6,17 @@ */ import type { ProductFeatureKeys, - ProductFeatureKibanaConfig, AssistantProductFeaturesConfigMap, } 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) => (): AssistantProductFeaturesConfigMap => { return createEnabledProductFeaturesConfigMap( - assistantProductFeaturesConfig, + assistantDefaultProductFeaturesConfig, 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 index c05e2287b6a5d..e7357833b608b 100644 --- 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 @@ -7,35 +7,16 @@ import type { ProductFeatureKeys, - ProductFeatureKibanaConfig, AttackDiscoveryProductFeaturesConfigMap, } 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) => (): AttackDiscoveryProductFeaturesConfigMap => createEnabledProductFeaturesConfigMap( - attackDiscoveryProductFeaturesConfig, + attackDiscoveryDefaultProductFeaturesConfig, 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 index ba19ec7cc73d8..ef9ababf861ea 100644 --- 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 @@ -5,14 +5,9 @@ * 2.0. */ import type { - ProductFeatureKibanaConfig, CasesProductFeaturesConfigMap, ProductFeatureKeys, } from '@kbn/security-solution-features'; -import type { - ProductFeatureCasesKey, - CasesSubFeatureId, -} from '@kbn/security-solution-features/keys'; import { getCasesDefaultProductFeaturesConfig, createEnabledProductFeaturesConfigMap, @@ -23,6 +18,11 @@ import { GET_CONNECTORS_CONFIGURE_API_TAG, } from '@kbn/cases-plugin/common/constants'; +const casesProductFeaturesConfig = getCasesDefaultProductFeaturesConfig({ + apiTags: { connectors: GET_CONNECTORS_CONFIGURE_API_TAG }, + uiCapabilities: { connectors: CASES_CONNECTORS_CAPABILITY }, +}); + export const getCasesProductFeaturesConfigurator = (enabledProductFeatureKeys: ProductFeatureKeys) => (): CasesProductFeaturesConfigMap => { return createEnabledProductFeaturesConfigMap( @@ -30,23 +30,3 @@ export const getCasesProductFeaturesConfigurator = 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/notes_product_features_config.ts b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/notes_product_features_config.ts index f2cde2d6b2fdf..6ce5140639eef 100644 --- 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 @@ -6,32 +6,16 @@ */ import type { ProductFeatureKeys, - ProductFeatureKibanaConfig, NotesProductFeaturesConfigMap, } 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) => (): NotesProductFeaturesConfigMap => - createEnabledProductFeaturesConfigMap(notesProductFeaturesConfig, enabledProductFeatureKeys); + createEnabledProductFeaturesConfigMap( + notesDefaultProductFeaturesConfig, + enabledProductFeatureKeys + ); diff --git a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/siem_migrations_product_features_config.ts b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/siem_migrations_product_features_config.ts index 4e1339048e5c8..59fe8312f5f8b 100644 --- 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 @@ -7,35 +7,16 @@ import type { ProductFeatureKeys, - ProductFeatureKibanaConfig, SiemMigrationsProductFeaturesConfigMap, } 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) => (): SiemMigrationsProductFeaturesConfigMap => createEnabledProductFeaturesConfigMap( - siemMigrationsProductFeaturesConfig, + siemMigrationsDefaultProductFeaturesConfig, 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 index 26fe891246abc..1df93490f2082 100644 --- 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 @@ -7,32 +7,16 @@ import type { ProductFeatureKeys, - ProductFeatureKibanaConfig, TimelineProductFeaturesConfigMap, } 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) => (): TimelineProductFeaturesConfigMap => - createEnabledProductFeaturesConfigMap(timelineProductFeaturesConfig, enabledProductFeatureKeys); + createEnabledProductFeaturesConfigMap( + timelineDefaultProductFeaturesConfig, + enabledProductFeatureKeys + ); 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 index 3cc06c0705aca..d101537a0812d 100644 --- 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 @@ -6,39 +6,17 @@ */ import type { ProductFeatureKeys, - ProductFeatureKibanaConfig, AssistantProductFeaturesConfigMap, } 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) => (): AssistantProductFeaturesConfigMap => { return createEnabledProductFeaturesConfigMap( - assistantProductFeaturesConfig, + assistantDefaultProductFeaturesConfig, 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 index 7ca34215a5594..04f11033c62ad 100644 --- 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 @@ -6,35 +6,16 @@ */ import type { ProductFeatureKeys, - ProductFeatureKibanaConfig, AttackDiscoveryProductFeaturesConfigMap, } 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) => (): AttackDiscoveryProductFeaturesConfigMap => createEnabledProductFeaturesConfigMap( - attackDiscoveryProductFeaturesConfig, + attackDiscoveryDefaultProductFeaturesConfig, 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 index 834bae961a767..efe6977b1dda2 100644 --- 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 @@ -5,7 +5,6 @@ * 2.0. */ import type { - ProductFeatureKibanaConfig, CasesProductFeaturesConfigMap, ProductFeatureKeys, } from '@kbn/security-solution-features'; @@ -13,15 +12,16 @@ 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'; +const casesProductFeaturesConfig = getCasesDefaultProductFeaturesConfig({ + apiTags: { connectors: GET_CONNECTORS_CONFIGURE_API_TAG }, + uiCapabilities: { connectors: CASES_CONNECTORS_CAPABILITY }, +}); + export const getCasesProductFeaturesConfigurator = (enabledProductFeatureKeys: ProductFeatureKeys) => (): CasesProductFeaturesConfigMap => { return createEnabledProductFeaturesConfigMap( @@ -29,23 +29,3 @@ export const getCasesProductFeaturesConfigurator = 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/notes_product_features_config.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/notes_product_features_config.ts index ca34d543cefca..6ce5140639eef 100644 --- 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 @@ -6,32 +6,16 @@ */ import type { ProductFeatureKeys, - ProductFeatureKibanaConfig, NotesProductFeaturesConfigMap, } 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) => (): NotesProductFeaturesConfigMap => - createEnabledProductFeaturesConfigMap(notesProductFeaturesConfig, enabledProductFeatureKeys); + createEnabledProductFeaturesConfigMap( + notesDefaultProductFeaturesConfig, + enabledProductFeatureKeys + ); 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 index 7717b8e735ff1..669dc55f88fc8 100644 --- 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 @@ -6,35 +6,16 @@ */ import type { ProductFeatureKeys, - ProductFeatureKibanaConfig, SiemMigrationsProductFeaturesConfigMap, } 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) => (): SiemMigrationsProductFeaturesConfigMap => createEnabledProductFeaturesConfigMap( - siemMigrationsProductFeaturesConfig, + siemMigrationsDefaultProductFeaturesConfig, 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 index 0f98f7bbba09c..b0a2c16bee012 100644 --- 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 @@ -6,32 +6,16 @@ */ import type { ProductFeatureKeys, - ProductFeatureKibanaConfig, TimelineProductFeaturesConfigMap, } 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) => (): TimelineProductFeaturesConfigMap => - createEnabledProductFeaturesConfigMap(timelineProductFeaturesConfig, enabledProductFeatureKeys); + createEnabledProductFeaturesConfigMap( + timelineDefaultProductFeaturesConfig, + enabledProductFeatureKeys + ); From ab7f382b8364b04034070ad5cf05b35eed99b648 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Mon, 28 Jul 2025 16:29:52 +0200 Subject: [PATCH 09/28] improve helper to create map and add test --- .../packages/features/src/helpers.test.ts | 72 +++++++++++++++++++ .../security/packages/features/src/helpers.ts | 29 ++++---- .../src/notes/product_feature_config.ts | 6 +- .../features/src/product_features_keys.ts | 12 ++-- .../src/timeline/product_feature_config.ts | 6 +- .../security/packages/features/src/types.ts | 11 ++- .../product_features_service.ts | 1 + .../assistant_product_features_config.ts | 2 + ...ttack_discovery_product_features_config.ts | 2 + .../cases_product_features_config.ts | 2 + .../notes_product_features_config.ts | 2 + .../security_product_features_config.ts | 1 + ...siem_migrations_product_features_config.ts | 2 + .../timeline_product_features_config.ts | 2 + .../assistant_product_features_config.ts | 2 + ...ttack_discovery_product_features_config.ts | 2 + .../cases_product_features_config.ts | 2 + .../notes_product_features_config.ts | 2 + .../security_product_features_config.ts | 1 + ...siem_migrations_product_features_config.ts | 2 + .../timeline_product_features_config.ts | 2 + 21 files changed, 132 insertions(+), 31 deletions(-) create mode 100644 x-pack/solutions/security/packages/features/src/helpers.test.ts diff --git a/x-pack/solutions/security/packages/features/src/helpers.test.ts b/x-pack/solutions/security/packages/features/src/helpers.test.ts new file mode 100644 index 0000000000000..d0545efcf6e83 --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/helpers.test.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { createEnabledProductFeaturesConfigMap } from './helpers'; +import { ProductFeatureSecurityKey } from './product_features_keys'; +import type { ProductFeatureKibanaConfig, ProductFeatureKeys } from './types'; + +const productFeaturesConfigs: Partial< + Record +> = { + [ProductFeatureSecurityKey.advancedInsights]: { + privileges: { all: { ui: ['capability1'] } }, + }, + [ProductFeatureSecurityKey.automaticImport]: { + privileges: { all: { ui: ['capability2'] } }, + }, + [ProductFeatureSecurityKey.detections]: { + privileges: { all: { ui: ['capability3'] } }, + }, +}; + +describe('createEnabledProductFeaturesConfigMap', () => { + it('should return a Map with only enabled product features and their configs', () => { + const enabledProductFeaturesKeys = [ + ProductFeatureSecurityKey.advancedInsights, + ProductFeatureSecurityKey.automaticImport, + ] as unknown as ProductFeatureKeys; + + const result = createEnabledProductFeaturesConfigMap( + ProductFeatureSecurityKey, + productFeaturesConfigs, + enabledProductFeaturesKeys + ); + + expect(result.size).toBe(2); + expect(result.get(ProductFeatureSecurityKey.advancedInsights)).toEqual({ + privileges: { all: { ui: ['capability1'] } }, + }); + expect(result.get(ProductFeatureSecurityKey.automaticImport)).toEqual({ + privileges: { all: { ui: ['capability2'] } }, + }); + expect(result.has(ProductFeatureSecurityKey.detections)).toBe(false); + }); + + it('should return empty config object if config is missing for enabled key', () => { + const enabledKeys: ProductFeatureKeys = [ + ProductFeatureSecurityKey.detections, + ] as unknown as ProductFeatureKeys; + + const result = createEnabledProductFeaturesConfigMap( + ProductFeatureSecurityKey, + {}, // No specific configs provided for "detections" ProductFeatureKey + enabledKeys + ); + + expect(result.size).toBe(1); + expect(result.get(ProductFeatureSecurityKey.detections)).toEqual({}); + }); + + it('should return an empty Map if no features are enabled', () => { + const result = createEnabledProductFeaturesConfigMap( + ProductFeatureSecurityKey, + productFeaturesConfigs, + [] + ); + + expect(result.size).toBe(0); + }); +}); diff --git a/x-pack/solutions/security/packages/features/src/helpers.ts b/x-pack/solutions/security/packages/features/src/helpers.ts index 1d093920796fa..078eeac5bd303 100644 --- a/x-pack/solutions/security/packages/features/src/helpers.ts +++ b/x-pack/solutions/security/packages/features/src/helpers.ts @@ -13,22 +13,27 @@ import type { /** * Creates the ProductFeaturesConfig Map from the given productFeatures object and a set of enabled productFeatures keys. + * + * @param productFeatureKeys - The specific ProductFeatureKey enum e.g. ProductFeatureSecurityKey + * @param productFeaturesConfigs - The product features configs object, no need to include all keys, only the ones that have a config + * @param enabledProductFeaturesKeys - The enabled product features keys + * @returns A Map of all the enabled product features configs */ export const createEnabledProductFeaturesConfigMap = < K extends ProductFeatureKeyType, T extends string = string >( - productFeatures: Partial>>, + productFeatureKeys: Record, + productFeaturesConfigs: Partial>>, enabledProductFeaturesKeys: ProductFeatureKeys -) => - new Map( - Object.entries(productFeatures).reduce]>>( - (acc, [key, value]) => { - if (enabledProductFeaturesKeys.includes(key as K)) { - acc.push([key as K, value as ProductFeatureKibanaConfig]); - } - return acc; - }, - [] - ) +) => { + const allProductFeatureKeys = Object.values(productFeatureKeys) as K[]; + return new Map( + allProductFeatureKeys.reduce]>>((acc, key) => { + if (enabledProductFeaturesKeys.includes(key)) { + acc.push([key, productFeaturesConfigs[key] ?? {}]); + } + return acc; + }, []) ); +}; 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 4107e88ed7dba..10853c70666de 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,7 +5,7 @@ * 2.0. */ -import { ProductFeatureNotesFeatureKey } from '../product_features_keys'; +import { ProductFeatureNotesKey } from '../product_features_keys'; import type { ProductFeaturesConfig } from '../types'; /** @@ -18,9 +18,9 @@ import type { ProductFeaturesConfig } from '../types'; * - `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: ProductFeaturesConfig = +export const notesDefaultProductFeaturesConfig: ProductFeaturesConfig = { - [ProductFeatureNotesFeatureKey.notes]: { + [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 67704a0d78aac..d09780b715311 100644 --- a/x-pack/solutions/security/packages/features/src/product_features_keys.ts +++ b/x-pack/solutions/security/packages/features/src/product_features_keys.ts @@ -132,14 +132,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 */ @@ -159,8 +159,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 = @@ -169,8 +169,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/timeline/product_feature_config.ts b/x-pack/solutions/security/packages/features/src/timeline/product_feature_config.ts index a35955492a6e4..8b56d51a312df 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,7 +5,7 @@ * 2.0. */ -import { ProductFeatureTimelineFeatureKey } from '../product_features_keys'; +import { ProductFeatureTimelineKey } from '../product_features_keys'; import type { ProductFeaturesConfig } from '../types'; /** @@ -18,9 +18,9 @@ import type { ProductFeaturesConfig } from '../types'; * - `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: ProductFeaturesConfig = +export const timelineDefaultProductFeaturesConfig: ProductFeaturesConfig = { - [ProductFeatureTimelineFeatureKey.timeline]: { + [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 305c64921be80..f4333e0ab6662 100644 --- a/x-pack/solutions/security/packages/features/src/types.ts +++ b/x-pack/solutions/security/packages/features/src/types.ts @@ -21,8 +21,8 @@ import type { CasesSubFeatureId, SecuritySubFeatureId, ProductFeatureSiemMigrationsKey, - ProductFeatureTimelineFeatureKey, - ProductFeatureNotesFeatureKey, + ProductFeatureTimelineKey, + ProductFeatureNotesKey, } from './product_features_keys'; export type { ProductFeatureKeyType }; @@ -87,14 +87,11 @@ export type AttackDiscoveryProductFeaturesConfigMap = Map< >; export type TimelineProductFeaturesConfigMap = Map< - ProductFeatureTimelineFeatureKey, + ProductFeatureTimelineKey, ProductFeatureKibanaConfig >; -export type NotesProductFeaturesConfigMap = Map< - ProductFeatureNotesFeatureKey, - ProductFeatureKibanaConfig ->; +export type NotesProductFeaturesConfigMap = Map; export type SiemMigrationsProductFeaturesConfigMap = Map< ProductFeatureSiemMigrationsKey, 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 0cc1311740bfd..a1a2c16b6385e 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 @@ -91,6 +91,7 @@ export class ProductFeaturesService { this.productFeaturesRegistry.register(featureGroup, productFeatureConfig); enabledProductFeatures.push(...productFeatureConfig.keys()); } + this.logger.debug(`Enabled product features: ${enabledProductFeatures.join(', ')}`); this.enabledProductFeatures = new Set(enabledProductFeatures); } 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 index d101537a0812d..e7f283b67a9a3 100644 --- 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 @@ -12,10 +12,12 @@ import { assistantDefaultProductFeaturesConfig, createEnabledProductFeaturesConfigMap, } from '@kbn/security-solution-features/config'; +import { ProductFeatureAssistantKey } from '@kbn/security-solution-features/keys'; export const getSecurityAssistantProductFeaturesConfigurator = (enabledProductFeatureKeys: ProductFeatureKeys) => (): AssistantProductFeaturesConfigMap => { return createEnabledProductFeaturesConfigMap( + ProductFeatureAssistantKey, assistantDefaultProductFeaturesConfig, enabledProductFeatureKeys ); 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 index e7357833b608b..86df2ade56e51 100644 --- 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 @@ -13,10 +13,12 @@ import { attackDiscoveryDefaultProductFeaturesConfig, createEnabledProductFeaturesConfigMap, } from '@kbn/security-solution-features/config'; +import { ProductFeatureAttackDiscoveryKey } from '@kbn/security-solution-features/keys'; export const getAttackDiscoveryProductFeaturesConfigurator = (enabledProductFeatureKeys: ProductFeatureKeys) => (): AttackDiscoveryProductFeaturesConfigMap => createEnabledProductFeaturesConfigMap( + ProductFeatureAttackDiscoveryKey, attackDiscoveryDefaultProductFeaturesConfig, 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 index ef9ababf861ea..974cee22ab4f7 100644 --- 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 @@ -17,6 +17,7 @@ import { CASES_CONNECTORS_CAPABILITY, GET_CONNECTORS_CONFIGURE_API_TAG, } from '@kbn/cases-plugin/common/constants'; +import { ProductFeatureCasesKey } from '@kbn/security-solution-features/keys'; const casesProductFeaturesConfig = getCasesDefaultProductFeaturesConfig({ apiTags: { connectors: GET_CONNECTORS_CONFIGURE_API_TAG }, @@ -26,6 +27,7 @@ const casesProductFeaturesConfig = getCasesDefaultProductFeaturesConfig({ export const getCasesProductFeaturesConfigurator = (enabledProductFeatureKeys: ProductFeatureKeys) => (): CasesProductFeaturesConfigMap => { return createEnabledProductFeaturesConfigMap( + ProductFeatureCasesKey, casesProductFeaturesConfig, enabledProductFeatureKeys ); 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 index 6ce5140639eef..fec45a186869e 100644 --- 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 @@ -12,10 +12,12 @@ import { notesDefaultProductFeaturesConfig, createEnabledProductFeaturesConfigMap, } from '@kbn/security-solution-features/config'; +import { ProductFeatureNotesKey } from '@kbn/security-solution-features/keys'; export const getNotesProductFeaturesConfigurator = (enabledProductFeatureKeys: ProductFeatureKeys) => (): NotesProductFeaturesConfigMap => createEnabledProductFeaturesConfigMap( + ProductFeatureNotesKey, notesDefaultProductFeaturesConfig, enabledProductFeatureKeys ); 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 index c028e227aa929..88a2feeb25f12 100644 --- 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 @@ -23,6 +23,7 @@ import type { SecurityProductFeaturesConfig } from '@kbn/security-solution-featu export const getSecurityProductFeaturesConfigurator = (enabledProductFeatureKeys: ProductFeatureKeys) => (): SecurityProductFeaturesConfigMap => { return createEnabledProductFeaturesConfigMap( + ProductFeatureSecurityKey, securityEssProductFeaturesConfig, enabledProductFeatureKeys ); diff --git a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/siem_migrations_product_features_config.ts b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/siem_migrations_product_features_config.ts index 59fe8312f5f8b..0fe95462dff38 100644 --- 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 @@ -13,10 +13,12 @@ import { siemMigrationsDefaultProductFeaturesConfig, createEnabledProductFeaturesConfigMap, } from '@kbn/security-solution-features/config'; +import { ProductFeatureSiemMigrationsKey } from '@kbn/security-solution-features/keys'; export const getSiemMigrationsProductFeaturesConfigurator = (enabledProductFeatureKeys: ProductFeatureKeys) => (): SiemMigrationsProductFeaturesConfigMap => createEnabledProductFeaturesConfigMap( + ProductFeatureSiemMigrationsKey, siemMigrationsDefaultProductFeaturesConfig, 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 index 1df93490f2082..ed1c8d555ea22 100644 --- 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 @@ -13,10 +13,12 @@ import { createEnabledProductFeaturesConfigMap, timelineDefaultProductFeaturesConfig, } from '@kbn/security-solution-features/config'; +import { ProductFeatureTimelineKey } from '@kbn/security-solution-features/keys'; export const getTimelineProductFeaturesConfigurator = (enabledProductFeatureKeys: ProductFeatureKeys) => (): TimelineProductFeaturesConfigMap => createEnabledProductFeaturesConfigMap( + ProductFeatureTimelineKey, timelineDefaultProductFeaturesConfig, enabledProductFeatureKeys ); 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 index d101537a0812d..e7f283b67a9a3 100644 --- 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 @@ -12,10 +12,12 @@ import { assistantDefaultProductFeaturesConfig, createEnabledProductFeaturesConfigMap, } from '@kbn/security-solution-features/config'; +import { ProductFeatureAssistantKey } from '@kbn/security-solution-features/keys'; export const getSecurityAssistantProductFeaturesConfigurator = (enabledProductFeatureKeys: ProductFeatureKeys) => (): AssistantProductFeaturesConfigMap => { return createEnabledProductFeaturesConfigMap( + ProductFeatureAssistantKey, assistantDefaultProductFeaturesConfig, enabledProductFeatureKeys ); 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 index 04f11033c62ad..acfadddbd16aa 100644 --- 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 @@ -12,10 +12,12 @@ import { attackDiscoveryDefaultProductFeaturesConfig, createEnabledProductFeaturesConfigMap, } from '@kbn/security-solution-features/config'; +import { ProductFeatureAttackDiscoveryKey } from '@kbn/security-solution-features/keys'; export const getAttackDiscoveryProductFeaturesConfigurator = (enabledProductFeatureKeys: ProductFeatureKeys) => (): AttackDiscoveryProductFeaturesConfigMap => createEnabledProductFeaturesConfigMap( + ProductFeatureAttackDiscoveryKey, attackDiscoveryDefaultProductFeaturesConfig, 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 index efe6977b1dda2..44026bec51659 100644 --- 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 @@ -16,6 +16,7 @@ import { CASES_CONNECTORS_CAPABILITY, GET_CONNECTORS_CONFIGURE_API_TAG, } from '@kbn/cases-plugin/common/constants'; +import { ProductFeatureCasesKey } from '@kbn/security-solution-features/keys'; const casesProductFeaturesConfig = getCasesDefaultProductFeaturesConfig({ apiTags: { connectors: GET_CONNECTORS_CONFIGURE_API_TAG }, @@ -25,6 +26,7 @@ const casesProductFeaturesConfig = getCasesDefaultProductFeaturesConfig({ export const getCasesProductFeaturesConfigurator = (enabledProductFeatureKeys: ProductFeatureKeys) => (): CasesProductFeaturesConfigMap => { return createEnabledProductFeaturesConfigMap( + ProductFeatureCasesKey, casesProductFeaturesConfig, enabledProductFeatureKeys ); 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 index 6ce5140639eef..fec45a186869e 100644 --- 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 @@ -12,10 +12,12 @@ import { notesDefaultProductFeaturesConfig, createEnabledProductFeaturesConfigMap, } from '@kbn/security-solution-features/config'; +import { ProductFeatureNotesKey } from '@kbn/security-solution-features/keys'; export const getNotesProductFeaturesConfigurator = (enabledProductFeatureKeys: ProductFeatureKeys) => (): NotesProductFeaturesConfigMap => createEnabledProductFeaturesConfigMap( + ProductFeatureNotesKey, notesDefaultProductFeaturesConfig, enabledProductFeatureKeys ); 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 index 72a2d369d0728..a12a33cecc5ad 100644 --- 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 @@ -28,6 +28,7 @@ export const getSecurityProductFeaturesConfigurator = ) => (): SecurityProductFeaturesConfigMap => { return createEnabledProductFeaturesConfigMap( + ProductFeatureSecurityKey, securityProductFeaturesConfig, enabledProductFeatureKeys ); 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 index 669dc55f88fc8..7ab03f596044e 100644 --- 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 @@ -12,10 +12,12 @@ import { siemMigrationsDefaultProductFeaturesConfig, createEnabledProductFeaturesConfigMap, } from '@kbn/security-solution-features/config'; +import { ProductFeatureSiemMigrationsKey } from '@kbn/security-solution-features/keys'; export const getSiemMigrationsProductFeaturesConfigurator = (enabledProductFeatureKeys: ProductFeatureKeys) => (): SiemMigrationsProductFeaturesConfigMap => createEnabledProductFeaturesConfigMap( + ProductFeatureSiemMigrationsKey, siemMigrationsDefaultProductFeaturesConfig, 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 index b0a2c16bee012..4e5691a928a3a 100644 --- 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 @@ -12,10 +12,12 @@ import { timelineDefaultProductFeaturesConfig, createEnabledProductFeaturesConfigMap, } from '@kbn/security-solution-features/config'; +import { ProductFeatureTimelineKey } from '@kbn/security-solution-features/keys'; export const getTimelineProductFeaturesConfigurator = (enabledProductFeatureKeys: ProductFeatureKeys) => (): TimelineProductFeaturesConfigMap => createEnabledProductFeaturesConfigMap( + ProductFeatureTimelineKey, timelineDefaultProductFeaturesConfig, enabledProductFeatureKeys ); From a1bb73fc3ad0cd2cbc17ccb9a257ac1b2de13803 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 28 Jul 2025 15:09:17 +0000 Subject: [PATCH 10/28] [CI] Auto-commit changed files from 'node scripts/eslint_all_files --no-cache --fix' --- .../src/notes/product_feature_config.ts | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) 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 10853c70666de..bdff573f1cd65 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 @@ -18,18 +18,17 @@ import type { ProductFeaturesConfig } from '../types'; * - `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: ProductFeaturesConfig = - { - [ProductFeatureNotesKey.notes]: { - privileges: { - all: { - api: ['notes_read', 'notes_write'], - ui: ['read', 'crud'], - }, - read: { - api: ['notes_read'], - ui: ['read'], - }, +export const notesDefaultProductFeaturesConfig: ProductFeaturesConfig = { + [ProductFeatureNotesKey.notes]: { + privileges: { + all: { + api: ['notes_read', 'notes_write'], + ui: ['read', 'crud'], + }, + read: { + api: ['notes_read'], + ui: ['read'], }, }, - }; + }, +}; From 7d340bd76e6465e33c6893a3ebbe675c86ea37a1 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Mon, 28 Jul 2025 18:28:41 +0200 Subject: [PATCH 11/28] fix lint issue --- .../src/notes/product_feature_config.ts | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) 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 10853c70666de..bdff573f1cd65 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 @@ -18,18 +18,17 @@ import type { ProductFeaturesConfig } from '../types'; * - `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: ProductFeaturesConfig = - { - [ProductFeatureNotesKey.notes]: { - privileges: { - all: { - api: ['notes_read', 'notes_write'], - ui: ['read', 'crud'], - }, - read: { - api: ['notes_read'], - ui: ['read'], - }, +export const notesDefaultProductFeaturesConfig: ProductFeaturesConfig = { + [ProductFeatureNotesKey.notes]: { + privileges: { + all: { + api: ['notes_read', 'notes_write'], + ui: ['read', 'crud'], + }, + read: { + api: ['notes_read'], + ui: ['read'], }, }, - }; + }, +}; From a9f1c327c71f5d7950727455e8168b8bbcb43e3c Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Tue, 29 Jul 2025 17:01:44 +0200 Subject: [PATCH 12/28] add tests --- .../security/packages/features/src/types.ts | 2 +- .../product_features.ts | 7 +- .../server/jest.config.js | 13 ++ .../security_product_features_config.test.ts | 109 +++++++++++++++++ .../security_product_features_config.ts | 75 ++++++------ .../security_product_features_config.test.ts | 113 ++++++++++++++++++ .../security_product_features_config.ts | 59 ++++----- 7 files changed, 314 insertions(+), 64 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution_ess/server/jest.config.js create mode 100644 x-pack/solutions/security/plugins/security_solution_ess/server/product_features/security_product_features_config.test.ts create mode 100644 x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/security_product_features_config.test.ts diff --git a/x-pack/solutions/security/packages/features/src/types.ts b/x-pack/solutions/security/packages/features/src/types.ts index f4333e0ab6662..6596c83031776 100644 --- a/x-pack/solutions/security/packages/features/src/types.ts +++ b/x-pack/solutions/security/packages/features/src/types.ts @@ -32,7 +32,7 @@ export type ProductFeatureKeys = ProductFeatureKeyType[]; export type BaseKibanaFeatureConfig = Omit; export type SubFeaturesPrivileges = RecursivePartial; -export type FeatureConfigModifier = (baseFeatureConfig: BaseKibanaFeatureConfig) => void; +export type FeatureConfigModifier = (config: KibanaFeatureConfig) => void; export type ProductFeatureKibanaConfig = RecursivePartial & { 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 99fab544a5b15..24b1cae5ac42a 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 @@ -62,7 +62,12 @@ export class ProductFeatures { Array.from(productFeatureConfig.values()) ); - this.logger.debug(() => JSON.stringify(completeProductFeatureConfig)); + if (completeProductFeatureConfig.id === 'siemV3') { + this.logger.debug(() => { + const config = JSON.stringify(completeProductFeatureConfig, null, 2); + return `Registering feature "${completeProductFeatureConfig.id}" with config: ${config}`; + }); + } this.featuresSetup.registerKibanaFeature(completeProductFeatureConfig); this.addRegisteredActions(completeProductFeatureConfig); } 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/product_features/security_product_features_config.test.ts b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/security_product_features_config.test.ts new file mode 100644 index 0000000000000..8ceedb36d0ecf --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/security_product_features_config.test.ts @@ -0,0 +1,109 @@ +/* + * 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 { KibanaFeatureConfig } from '@kbn/features-plugin/common'; +import { endpointArtifactManagementFeatureConfigModifier } from './security_product_features_config'; +import { SECURITY_FEATURE_ID_V3 } from '@kbn/security-solution-features/constants'; + +const baseFeatureConfig = { + id: SECURITY_FEATURE_ID_V3, + name: 'Security Feature V3', + 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('endpointArtifactManagementFeatureConfigModifier', () => { + it('should modify privileges for SECURITY_FEATURE_ID_V3 in both default and minimal', () => { + const featureConfig = { + ...baseFeatureConfig, + privileges: { + ...baseFeatureConfig.privileges, + all: { + ...baseFeatureConfig.privileges.all, + replacedBy: { + default: [ + { feature: SECURITY_FEATURE_ID_V3, privileges: ['all'] }, + { feature: 'other_feature', privileges: ['all'] }, + ], + minimal: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['all'] }], + }, + }, + }, + }; + + endpointArtifactManagementFeatureConfigModifier(featureConfig); + + const replacedBy = featureConfig.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 do nothing if replacedBy is not present', () => { + const originalConfig = JSON.parse(JSON.stringify(baseFeatureConfig)); + + endpointArtifactManagementFeatureConfigModifier(baseFeatureConfig as KibanaFeatureConfig); + + expect(baseFeatureConfig).toEqual(originalConfig); + }); + + it('should only modify existing SECURITY_FEATURE_ID_V3 entries', () => { + const featureConfig = { + ...baseFeatureConfig, + privileges: { + ...baseFeatureConfig.privileges, + all: { + ...baseFeatureConfig.privileges.all, + replacedBy: { + default: [{ feature: 'other_feature', privileges: ['all'] }], + minimal: [{ feature: 'other_feature', privileges: ['all'] }], + }, + }, + }, + }; + + endpointArtifactManagementFeatureConfigModifier(featureConfig); + + const replacedBy = featureConfig.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/security_product_features_config.ts b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/security_product_features_config.ts index 88a2feeb25f12..c6115f9d4d481 100644 --- 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 @@ -5,6 +5,7 @@ * 2.0. */ import type { + FeatureConfigModifier, ProductFeatureKeys, SecurityProductFeaturesConfigMap, } from '@kbn/security-solution-features'; @@ -24,12 +25,12 @@ export const getSecurityProductFeaturesConfigurator = (enabledProductFeatureKeys: ProductFeatureKeys) => (): SecurityProductFeaturesConfigMap => { return createEnabledProductFeaturesConfigMap( ProductFeatureSecurityKey, - securityEssProductFeaturesConfig, + getSecurityEssProductFeaturesConfig(), enabledProductFeatureKeys ); }; -const securityEssProductFeaturesConfig: SecurityProductFeaturesConfig = { +const getSecurityEssProductFeaturesConfig = (): SecurityProductFeaturesConfig => ({ ...securityDefaultProductFeaturesConfig, [ProductFeatureSecurityKey.endpointExceptions]: { @@ -58,39 +59,43 @@ const securityEssProductFeaturesConfig: SecurityProductFeaturesConfig = { SecuritySubFeatureId.globalArtifactManagement, ], - // When endpointArtifactManagement PLI is enabled, the replacedBy for the siemV3 feature needs to - // account for the privileges of the sub-features that are introduced by it. - featureConfigModifier: (baseFeatureConfig) => { - const replacedBy = baseFeatureConfig.privileges?.all?.replacedBy; - if (!replacedBy) { - return; - } + featureConfigModifier: endpointArtifactManagementFeatureConfigModifier, + }, +}); - if ('default' in replacedBy) { - 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', - 'global_artifact_management_all', // Enabling sub-features toggle to show that Global Artifact Management is now provided to the user. - ]; - } - } +// When endpointArtifactManagement PLI is enabled, the replacedBy for the siemV3 feature needs to +// account for the privileges of the sub-features that are introduced by it. +export const endpointArtifactManagementFeatureConfigModifier: FeatureConfigModifier = ( + featureConfig +) => { + const replacedBy = featureConfig.privileges?.all?.replacedBy; + if (!replacedBy) { + return; + } - if ('minimal' in replacedBy) { - const v3Minimal = replacedBy.minimal.find( - ({ feature }) => feature === SECURITY_FEATURE_ID_V3 // Only for features that are replaced by siemV3 (siem and siemV2) - ); - 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 - ]; - } - } - }, - }, + if ('default' in replacedBy) { + 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', + '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 // Only for features that are replaced by siemV3 (siem and siemV2) + ); + 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_serverless/server/product_features/security_product_features_config.test.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/security_product_features_config.test.ts new file mode 100644 index 0000000000000..e32e0a0261e9d --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/security_product_features_config.test.ts @@ -0,0 +1,113 @@ +/* + * 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 { KibanaFeatureConfig } from '@kbn/features-plugin/common'; +import { endpointArtifactManagementFeatureConfigModifier } from './security_product_features_config'; +import { SECURITY_FEATURE_ID_V3 } from '@kbn/security-solution-features/constants'; + +const baseFeatureConfig = { + id: SECURITY_FEATURE_ID_V3, + name: 'Security Feature V3', + 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('endpointArtifactManagementFeatureConfigModifier', () => { + it('should modify privileges for SECURITY_FEATURE_ID_V3 in both default and minimal', () => { + const featureConfig = { + ...baseFeatureConfig, + privileges: { + ...baseFeatureConfig.privileges, + all: { + ...baseFeatureConfig.privileges.all, + replacedBy: { + default: [ + { feature: SECURITY_FEATURE_ID_V3, privileges: ['all'] }, + { feature: 'other_feature', privileges: ['all'] }, + ], + minimal: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['all'] }], + }, + }, + }, + }; + + endpointArtifactManagementFeatureConfigModifier(featureConfig); + + const replacedBy = featureConfig.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', + ]); + + // Minimal privileges modified + const v3Minimal = replacedBy.minimal.find( + ({ feature }: { feature: string }) => feature === SECURITY_FEATURE_ID_V3 + ); + expect(v3Minimal?.privileges).toEqual(['all']); + + // Ensure other features remain unchanged + const otherFeature = replacedBy.default.find( + ({ feature }: { feature: string }) => feature === 'other_feature' + ); + expect(otherFeature?.privileges).toEqual(['all']); + }); + + it('should do nothing if replacedBy is not present', () => { + const originalConfig = JSON.parse(JSON.stringify(baseFeatureConfig)); + + endpointArtifactManagementFeatureConfigModifier(baseFeatureConfig as KibanaFeatureConfig); + + expect(baseFeatureConfig).toEqual(originalConfig); + }); + + it('should only modify existing SECURITY_FEATURE_ID_V3 entries', () => { + const featureConfig = { + ...baseFeatureConfig, + privileges: { + ...baseFeatureConfig.privileges, + all: { + ...baseFeatureConfig.privileges.all, + replacedBy: { + default: [{ feature: 'other_feature', privileges: ['all'] }], + minimal: [{ feature: 'other_feature', privileges: ['all'] }], + }, + }, + }, + }; + + endpointArtifactManagementFeatureConfigModifier(featureConfig); + + const replacedBy = featureConfig.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/security_product_features_config.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/security_product_features_config.ts index a12a33cecc5ad..76677e625b4ef 100644 --- 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 @@ -5,6 +5,7 @@ * 2.0. */ import type { + FeatureConfigModifier, ProductFeatureKeys, SecurityProductFeaturesConfigMap, } from '@kbn/security-solution-features'; @@ -29,12 +30,12 @@ export const getSecurityProductFeaturesConfigurator = (): SecurityProductFeaturesConfigMap => { return createEnabledProductFeaturesConfigMap( ProductFeatureSecurityKey, - securityProductFeaturesConfig, + getSecurityProductFeaturesConfig(), enabledProductFeatureKeys ); }; -const securityProductFeaturesConfig: SecurityProductFeaturesConfig = { +const getSecurityProductFeaturesConfig = (): SecurityProductFeaturesConfig => ({ ...securityDefaultProductFeaturesConfig, [ProductFeatureSecurityKey.endpointExceptions]: { @@ -54,30 +55,34 @@ const securityProductFeaturesConfig: SecurityProductFeaturesConfig = { SecuritySubFeatureId.globalArtifactManagement, ], - // When endpointArtifactManagement PLI is enabled, the replacedBy for the siemV3 feature needs to - // account for the privileges of the sub-features that are introduced by it. - featureConfigModifier: (baseFeatureConfig) => { - const replacedBy = baseFeatureConfig.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', - ]; - } - }, + featureConfigModifier: endpointArtifactManagementFeatureConfigModifier, }, +}); + +// When endpointArtifactManagement PLI is enabled, the replacedBy for the siemV3 feature needs to +// account for the privileges of the sub-features that are introduced by it. +export const endpointArtifactManagementFeatureConfigModifier: FeatureConfigModifier = ( + featureConfig +) => { + 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', + ]; + } }; From 971f22bed0e987e91952c547d8508247e15c706b Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Tue, 29 Jul 2025 21:06:31 +0200 Subject: [PATCH 13/28] add tests for the features modifiers in ess and serverless --- .../shared/kbn-utility-types/index.ts | 6 + .../security/packages/features/src/types.ts | 7 +- .../product_features.ts | 6 - .../product_features_config_merger.ts | 12 +- .../security_product_features_config.test.ts | 182 ++++++++++------ .../security_product_features_config.ts | 32 +-- .../security_product_features_config.test.ts | 194 +++++++++++------- .../security_product_features_config.ts | 24 ++- 8 files changed, 295 insertions(+), 168 deletions(-) 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/src/types.ts b/x-pack/solutions/security/packages/features/src/types.ts index 6596c83031776..9bcce20138068 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, @@ -32,7 +32,10 @@ export type ProductFeatureKeys = ProductFeatureKeyType[]; export type BaseKibanaFeatureConfig = Omit; export type SubFeaturesPrivileges = RecursivePartial; -export type FeatureConfigModifier = (config: KibanaFeatureConfig) => void; +export type MutableKibanaFeatureConfig = RecursiveWritable; +export type MutableSubFeatureConfig = RecursiveWritable; + +export type FeatureConfigModifier = (config: MutableKibanaFeatureConfig) => void; export type ProductFeatureKibanaConfig = RecursivePartial & { 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 24b1cae5ac42a..db707b2e0f2d1 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 @@ -62,12 +62,6 @@ export class ProductFeatures { Array.from(productFeatureConfig.values()) ); - if (completeProductFeatureConfig.id === 'siemV3') { - this.logger.debug(() => { - const config = JSON.stringify(completeProductFeatureConfig, null, 2); - return `Registering feature "${completeProductFeatureConfig.id}" with config: ${config}`; - }); - } this.featuresSetup.registerKibanaFeature(completeProductFeatureConfig); this.addRegisteredActions(completeProductFeatureConfig); } 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 40994b5e59840..7bfbaa810846f 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 @@ -13,6 +13,8 @@ import type { BaseKibanaFeatureConfig, SubFeaturesPrivileges, FeatureConfigModifier, + MutableKibanaFeatureConfig, + MutableSubFeatureConfig, } from '@kbn/security-solution-features'; export class ProductFeaturesConfigMerger { @@ -33,7 +35,7 @@ export class ProductFeaturesConfigMerger { kibanaSubFeatureIds: T[], productFeaturesConfigs: ProductFeatureKibanaConfig[] ): KibanaFeatureConfig { - const mergedKibanaFeatureConfig = cloneDeep(kibanaFeatureConfig) as KibanaFeatureConfig; + const mergedKibanaFeatureConfig = cloneDeep(kibanaFeatureConfig) as MutableKibanaFeatureConfig; const enabledSubFeaturesIndexed = Object.fromEntries( kibanaSubFeatureIds.map((id) => [id, true]) @@ -47,7 +49,7 @@ export class ProductFeaturesConfigMerger { subFeatureIds, featureConfigModifier, ...productFeatureConfigToMerge - } = cloneDeep(productFeatureConfig); + } = productFeatureConfig; subFeatureIds?.forEach((subFeatureId) => { enabledSubFeaturesIndexed[subFeatureId] = true; @@ -65,10 +67,10 @@ export class ProductFeaturesConfigMerger { }); // 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); } }); @@ -84,7 +86,7 @@ export class ProductFeaturesConfigMerger { modifier(mergedKibanaFeatureConfig); }); - return mergedKibanaFeatureConfig; + return Object.freeze(mergedKibanaFeatureConfig) as KibanaFeatureConfig; } /** diff --git a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/security_product_features_config.test.ts b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/security_product_features_config.test.ts index 8ceedb36d0ecf..983428cf8bf4e 100644 --- a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/security_product_features_config.test.ts +++ b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/security_product_features_config.test.ts @@ -4,13 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { KibanaFeatureConfig } from '@kbn/features-plugin/common'; -import { endpointArtifactManagementFeatureConfigModifier } from './security_product_features_config'; +import { updateGlobalArtifactManagerPrivileges } from './security_product_features_config'; import { SECURITY_FEATURE_ID_V3 } from '@kbn/security-solution-features/constants'; +import type { MutableKibanaFeatureConfig } from '@kbn/security-solution-features'; +import { APP_ID } from '@kbn/security-solution-plugin/common'; +import { cloneDeep } from 'lodash'; -const baseFeatureConfig = { - id: SECURITY_FEATURE_ID_V3, - name: 'Security Feature V3', +const baseFeatureConfig: MutableKibanaFeatureConfig = { + id: '[setTestFeatureId]', + name: 'Security Feature', app: ['securitySolution'], category: { id: 'security', label: 'Security' }, privileges: { @@ -33,77 +35,137 @@ const baseFeatureConfig = { }, }; -describe('endpointArtifactManagementFeatureConfigModifier', () => { - it('should modify privileges for SECURITY_FEATURE_ID_V3 in both default and minimal', () => { - const featureConfig = { - ...baseFeatureConfig, - privileges: { - ...baseFeatureConfig.privileges, - all: { - ...baseFeatureConfig.privileges.all, - replacedBy: { - default: [ - { feature: SECURITY_FEATURE_ID_V3, privileges: ['all'] }, - { feature: 'other_feature', privileges: ['all'] }, - ], - minimal: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['all'] }], +describe('updateGlobalArtifactManagerPrivileges', () => { + describe.each(['siem', 'siemV2'])('when features id is %s', (featureId: string) => { + let featureConfig: MutableKibanaFeatureConfig; + + beforeEach(() => { + featureConfig = { ...cloneDeep(baseFeatureConfig), id: featureId }; + }); + + it('should do nothing if replacedBy is not present', () => { + const originalConfig = JSON.parse(JSON.stringify(featureConfig)); + + updateGlobalArtifactManagerPrivileges(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'] }], + }, }, }, - }, - }; + }; - endpointArtifactManagementFeatureConfigModifier(featureConfig); + updateGlobalArtifactManagerPrivileges(testFeatureConfig as MutableKibanaFeatureConfig); - const replacedBy = featureConfig.privileges.all.replacedBy; + 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']); + // 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']); + // 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']); - }); + // 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'] }], + }, + }, + }, + }; - it('should do nothing if replacedBy is not present', () => { - const originalConfig = JSON.parse(JSON.stringify(baseFeatureConfig)); + updateGlobalArtifactManagerPrivileges(testFeatureConfig as MutableKibanaFeatureConfig); - endpointArtifactManagementFeatureConfigModifier(baseFeatureConfig as KibanaFeatureConfig); + const replacedBy = testFeatureConfig.privileges.all.replacedBy; - expect(baseFeatureConfig).toEqual(originalConfig); + // No SECURITY_FEATURE_ID_V3, so no changes + expect(replacedBy.default[0].privileges).toEqual(['all']); + expect(replacedBy.minimal[0].privileges).toEqual(['all']); + }); + + it('should add writeGlobalArtifacts api action', () => { + const testFeatureConfig = { + ...featureConfig, + privileges: { + ...featureConfig.privileges, + all: { + ...featureConfig.privileges?.all, + replacedBy: { default: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['all'] }] }, + }, + }, + }; + + updateGlobalArtifactManagerPrivileges(testFeatureConfig as MutableKibanaFeatureConfig); + + expect(featureConfig.privileges?.all.api).toContain(`${APP_ID}-writeGlobalArtifacts`); + }); }); - it('should only modify existing SECURITY_FEATURE_ID_V3 entries', () => { - const featureConfig = { - ...baseFeatureConfig, - privileges: { - ...baseFeatureConfig.privileges, - all: { - ...baseFeatureConfig.privileges.all, - replacedBy: { - default: [{ feature: 'other_feature', privileges: ['all'] }], - minimal: [{ feature: 'other_feature', privileges: ['all'] }], + describe('when feature id is siemV3', () => { + it('should not modify privileges', () => { + const featureConfig = { + ...baseFeatureConfig, + id: 'siemV3', + privileges: { + ...baseFeatureConfig.privileges, + all: { + ...baseFeatureConfig.privileges?.all, + replacedBy: { + default: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['all'] }], + minimal: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['all'] }], + }, }, }, - }, - }; + }; + + updateGlobalArtifactManagerPrivileges(featureConfig as MutableKibanaFeatureConfig); + + const replacedBy = featureConfig.privileges.all.replacedBy; + + // No changes to SECURITY_FEATURE_ID_V3 + expect(replacedBy.default[0].privileges).toEqual(['all']); + expect(replacedBy.minimal[0].privileges).toEqual(['all']); + }); - endpointArtifactManagementFeatureConfigModifier(featureConfig); + it('should not add writeGlobalArtifacts api action', () => { + const featureConfig = { ...baseFeatureConfig, id: 'siemV3' }; - const replacedBy = featureConfig.privileges.all.replacedBy; + updateGlobalArtifactManagerPrivileges(featureConfig as MutableKibanaFeatureConfig); - // No SECURITY_FEATURE_ID_V3, so no changes - expect(replacedBy.default[0].privileges).toEqual(['all']); - expect(replacedBy.minimal[0].privileges).toEqual(['all']); + expect(featureConfig.privileges?.all.api).not.toContain(`${APP_ID}-writeGlobalArtifacts`); + }); }); }); 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 index c6115f9d4d481..769ee9b6e81ce 100644 --- 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 @@ -47,10 +47,6 @@ const getSecurityEssProductFeaturesConfig = (): SecurityProductFeaturesConfig => }, [ProductFeatureSecurityKey.endpointArtifactManagement]: { - privileges: { - all: { api: [`${APP_ID}-writeGlobalArtifacts`] }, - }, - subFeatureIds: [ SecuritySubFeatureId.hostIsolationExceptionsBasic, SecuritySubFeatureId.trustedApplications, @@ -59,24 +55,26 @@ const getSecurityEssProductFeaturesConfig = (): SecurityProductFeaturesConfig => SecuritySubFeatureId.globalArtifactManagement, ], - featureConfigModifier: endpointArtifactManagementFeatureConfigModifier, + featureConfigModifier: (featureConfig) => { + // When endpointArtifactManagement PLI is enabled, the replacedBy to the siemV3 feature needs to + // account for the privileges of the sub-features that are introduced by it. + updateGlobalArtifactManagerPrivileges(featureConfig); + }, }, }); -// When endpointArtifactManagement PLI is enabled, the replacedBy for the siemV3 feature needs to -// account for the privileges of the sub-features that are introduced by it. -export const endpointArtifactManagementFeatureConfigModifier: FeatureConfigModifier = ( - featureConfig -) => { +export const updateGlobalArtifactManagerPrivileges: FeatureConfigModifier = (featureConfig) => { + if (!['siem', 'siemV2'].includes(featureConfig.id)) { + return; + } + const replacedBy = featureConfig.privileges?.all?.replacedBy; if (!replacedBy) { return; } if ('default' in replacedBy) { - const v3Default = replacedBy.default.find( - ({ feature }) => feature === SECURITY_FEATURE_ID_V3 // Only for features that are replaced by siemV3 (siem and siemV2) - ); + 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 = [ @@ -87,9 +85,7 @@ export const endpointArtifactManagementFeatureConfigModifier: FeatureConfigModif } if ('minimal' in replacedBy) { - const v3Minimal = replacedBy.minimal.find( - ({ feature }) => feature === SECURITY_FEATURE_ID_V3 // Only for features that are replaced by siemV3 (siem and siemV2) - ); + 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 = [ @@ -98,4 +94,8 @@ export const endpointArtifactManagementFeatureConfigModifier: FeatureConfigModif ]; } } + + // Add the global artifact management API privilege to the all privileges of siem and siemV2 features for backwards compatibility + // No need to add the ui capability since they are automatically added by the Kibana features framework via the `replacedBy` field. + featureConfig.privileges?.all.api?.push(`${APP_ID}-writeGlobalArtifacts`); }; diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/security_product_features_config.test.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/security_product_features_config.test.ts index e32e0a0261e9d..aab02c52b4188 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/security_product_features_config.test.ts +++ b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/security_product_features_config.test.ts @@ -4,13 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { KibanaFeatureConfig } from '@kbn/features-plugin/common'; -import { endpointArtifactManagementFeatureConfigModifier } from './security_product_features_config'; +import { updateGlobalArtifactManagerPrivileges } from './security_product_features_config'; import { SECURITY_FEATURE_ID_V3 } from '@kbn/security-solution-features/constants'; +import type { MutableKibanaFeatureConfig } from '@kbn/security-solution-features'; +import { APP_ID } from '@kbn/security-solution-plugin/common'; +import { cloneDeep } from 'lodash'; -const baseFeatureConfig = { - id: SECURITY_FEATURE_ID_V3, - name: 'Security Feature V3', +const baseFeatureConfig: MutableKibanaFeatureConfig = { + id: '[setTestFeatureId]', + name: 'Security Feature', app: ['securitySolution'], category: { id: 'security', label: 'Security' }, privileges: { @@ -33,81 +35,135 @@ const baseFeatureConfig = { }, }; -describe('endpointArtifactManagementFeatureConfigModifier', () => { - it('should modify privileges for SECURITY_FEATURE_ID_V3 in both default and minimal', () => { - const featureConfig = { - ...baseFeatureConfig, - privileges: { - ...baseFeatureConfig.privileges, - all: { - ...baseFeatureConfig.privileges.all, - replacedBy: { - default: [ - { feature: SECURITY_FEATURE_ID_V3, privileges: ['all'] }, - { feature: 'other_feature', privileges: ['all'] }, - ], - minimal: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['all'] }], +describe('updateGlobalArtifactManagerPrivileges', () => { + describe.each(['siem', 'siemV2'])('when features id is %s', (featureId: string) => { + let featureConfig: MutableKibanaFeatureConfig; + + beforeEach(() => { + featureConfig = { ...cloneDeep(baseFeatureConfig), id: featureId }; + }); + + it('should do nothing if replacedBy is not present', () => { + const originalConfig = JSON.parse(JSON.stringify(featureConfig)); + + updateGlobalArtifactManagerPrivileges(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'] }], + }, }, }, - }, - }; - - endpointArtifactManagementFeatureConfigModifier(featureConfig); - - const replacedBy = featureConfig.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', - ]); - - // Minimal privileges modified - const v3Minimal = replacedBy.minimal.find( - ({ feature }: { feature: string }) => feature === SECURITY_FEATURE_ID_V3 - ); - expect(v3Minimal?.privileges).toEqual(['all']); - - // Ensure other features remain unchanged - const otherFeature = replacedBy.default.find( - ({ feature }: { feature: string }) => feature === 'other_feature' - ); - expect(otherFeature?.privileges).toEqual(['all']); - }); + }; + + updateGlobalArtifactManagerPrivileges(testFeatureConfig as MutableKibanaFeatureConfig); + + const replacedBy = testFeatureConfig.privileges.all.replacedBy; - it('should do nothing if replacedBy is not present', () => { - const originalConfig = JSON.parse(JSON.stringify(baseFeatureConfig)); + // 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', + ]); - endpointArtifactManagementFeatureConfigModifier(baseFeatureConfig as KibanaFeatureConfig); + // 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'] }], + }, + }, + }, + }; - expect(baseFeatureConfig).toEqual(originalConfig); + updateGlobalArtifactManagerPrivileges(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']); + }); + + it('should add writeGlobalArtifacts api action', () => { + const testFeatureConfig = { + ...featureConfig, + privileges: { + ...featureConfig.privileges, + all: { + ...featureConfig.privileges?.all, + replacedBy: { default: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['all'] }] }, + }, + }, + }; + + updateGlobalArtifactManagerPrivileges(testFeatureConfig as MutableKibanaFeatureConfig); + + expect(featureConfig.privileges?.all.api).toContain(`${APP_ID}-writeGlobalArtifacts`); + }); }); - it('should only modify existing SECURITY_FEATURE_ID_V3 entries', () => { - const featureConfig = { - ...baseFeatureConfig, - privileges: { - ...baseFeatureConfig.privileges, - all: { - ...baseFeatureConfig.privileges.all, - replacedBy: { - default: [{ feature: 'other_feature', privileges: ['all'] }], - minimal: [{ feature: 'other_feature', privileges: ['all'] }], + describe('when feature id is siemV3', () => { + it('should not modify privileges', () => { + const featureConfig = { + ...baseFeatureConfig, + id: 'siemV3', + privileges: { + ...baseFeatureConfig.privileges, + all: { + ...baseFeatureConfig.privileges?.all, + replacedBy: { + default: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['all'] }], + minimal: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['all'] }], + }, }, }, - }, - }; + }; + + updateGlobalArtifactManagerPrivileges(featureConfig as MutableKibanaFeatureConfig); + + const replacedBy = featureConfig.privileges.all.replacedBy; + + // No changes to SECURITY_FEATURE_ID_V3 + expect(replacedBy.default[0].privileges).toEqual(['all']); + expect(replacedBy.minimal[0].privileges).toEqual(['all']); + }); - endpointArtifactManagementFeatureConfigModifier(featureConfig); + it('should not add writeGlobalArtifacts api action', () => { + const featureConfig = { ...baseFeatureConfig, id: 'siemV3' }; - const replacedBy = featureConfig.privileges.all.replacedBy; + updateGlobalArtifactManagerPrivileges(featureConfig as MutableKibanaFeatureConfig); - // No SECURITY_FEATURE_ID_V3, so no changes - expect(replacedBy.default[0].privileges).toEqual(['all']); - expect(replacedBy.minimal[0].privileges).toEqual(['all']); + expect(featureConfig.privileges?.all.api).not.toContain(`${APP_ID}-writeGlobalArtifacts`); + }); }); }); 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 index 76677e625b4ef..3b0ca66a0f397 100644 --- 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 @@ -43,10 +43,6 @@ const getSecurityProductFeaturesConfig = (): SecurityProductFeaturesConfig => ({ }, [ProductFeatureSecurityKey.endpointArtifactManagement]: { - privileges: { - all: { api: [`${APP_ID}-writeGlobalArtifacts`] }, - }, - subFeatureIds: [ SecuritySubFeatureId.hostIsolationExceptionsBasic, SecuritySubFeatureId.trustedApplications, @@ -55,15 +51,19 @@ const getSecurityProductFeaturesConfig = (): SecurityProductFeaturesConfig => ({ SecuritySubFeatureId.globalArtifactManagement, ], - featureConfigModifier: endpointArtifactManagementFeatureConfigModifier, + featureConfigModifier: (featureConfig) => { + // When endpointArtifactManagement PLI is enabled, the replacedBy for the siemV3 feature needs to + // account for the privileges of the sub-features that are introduced by it. + updateGlobalArtifactManagerPrivileges(featureConfig); + }, }, }); -// When endpointArtifactManagement PLI is enabled, the replacedBy for the siemV3 feature needs to -// account for the privileges of the sub-features that are introduced by it. -export const endpointArtifactManagementFeatureConfigModifier: FeatureConfigModifier = ( - featureConfig -) => { +export const updateGlobalArtifactManagerPrivileges: FeatureConfigModifier = (featureConfig) => { + if (!['siem', 'siemV2'].includes(featureConfig.id)) { + return; + } + const replacedBy = featureConfig.privileges?.all?.replacedBy; if (!replacedBy || !('default' in replacedBy)) { return; @@ -85,4 +85,8 @@ export const endpointArtifactManagementFeatureConfigModifier: FeatureConfigModif 'endpoint_exceptions_all', ]; } + + // Add the global artifact management API privilege to the all privileges of siem and siemV2 features for backwards compatibility + // No need to add the ui capability since they are automatically added by the Kibana features framework via the `replacedBy` field. + featureConfig.privileges?.all.api?.push(`${APP_ID}-writeGlobalArtifacts`); }; From 37170ca298fc0261679009b8cf9c390d1affee4f Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Wed, 30 Jul 2025 20:48:43 +0200 Subject: [PATCH 14/28] implement extensions --- .../security/packages/features/config.ts | 16 --- .../packages/features/src/assistant/index.ts | 6 +- .../src/assistant/product_feature_config.ts | 12 +-- .../features/src/attack_discovery/index.ts | 14 ++- .../product_feature_config.ts | 12 +-- .../packages/features/src/cases/index.ts | 15 +-- .../src/cases/product_feature_config.ts | 27 ++--- .../packages/features/src/cases/types.ts | 12 ++- .../src/cases/v1_features/kibana_features.ts | 19 ++-- .../cases/v1_features/kibana_sub_features.ts | 13 +-- .../features/src/cases/v1_features/types.ts | 14 --- .../src/cases/v2_features/kibana_features.ts | 10 +- .../cases/v2_features/kibana_sub_features.ts | 14 +-- .../src/cases/v3_features/kibana_features.ts | 10 +- .../cases/v3_features/kibana_sub_features.ts | 16 +-- .../packages/features/src/helpers.test.ts | 2 +- .../security/packages/features/src/helpers.ts | 39 ------- .../packages/features/src/notes/index.ts | 2 + .../src/notes/product_feature_config.ts | 12 +-- .../packages/features/src/security/index.ts | 20 ++-- .../src/security/product_feature_config.ts | 21 ++-- .../v1_features/product_feature_config.ts | 23 ++++ .../v2_features/product_feature_config.ts | 23 ++++ .../features/src/siem_migrations/index.ts | 2 + .../siem_migrations/product_feature_config.ts | 12 +-- .../packages/features/src/timeline/index.ts | 2 + .../src/timeline/product_feature_config.ts | 35 +++--- .../security/packages/features/src/tools.ts | 31 ++++++ .../security/packages/features/src/types.ts | 94 +++++++++------- .../security/packages/features/tools.ts | 7 ++ .../cases_product_feature_params.ts | 27 +++-- .../product_features.ts | 73 ++++++++----- .../product_features_service.ts | 21 ++-- .../security_solution_ess/server/plugin.ts | 9 +- .../assistant_product_features_config.ts | 24 ----- ...ttack_discovery_product_features_config.ts | 24 ----- .../cases_product_features_config.ts | 34 ------ .../server/product_features/index.ts | 56 ++++++---- .../notes_product_features_config.ts | 23 ---- .../security_product_features_config.ts | 101 ------------------ ...siem_migrations_product_features_config.ts | 24 ----- .../timeline_product_features_config.ts | 24 ----- ...date_global_artifact_replacements.test.ts} | 16 +-- .../update_global_artifact_replacements.ts | 40 +++++++ .../assistant_product_features_config.ts | 24 ----- ...ttack_discovery_product_features_config.ts | 23 ---- .../cases_product_features_config.ts | 33 ------ .../server/product_features/index.ts | 46 ++++---- .../notes_product_features_config.ts | 23 ---- .../security_product_features_config.ts | 92 ---------------- ...siem_migrations_product_features_config.ts | 23 ---- .../timeline_product_features_config.ts | 23 ---- ...date_global_artifact_replacements.test.ts} | 2 +- .../update_global_artifact_replacements.ts | 35 ++++++ 54 files changed, 482 insertions(+), 873 deletions(-) delete mode 100644 x-pack/solutions/security/packages/features/config.ts delete mode 100644 x-pack/solutions/security/packages/features/src/cases/v1_features/types.ts delete mode 100644 x-pack/solutions/security/packages/features/src/helpers.ts create mode 100644 x-pack/solutions/security/packages/features/src/security/v1_features/product_feature_config.ts create mode 100644 x-pack/solutions/security/packages/features/src/security/v2_features/product_feature_config.ts create mode 100644 x-pack/solutions/security/packages/features/src/tools.ts create mode 100644 x-pack/solutions/security/packages/features/tools.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution_ess/server/product_features/assistant_product_features_config.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution_ess/server/product_features/attack_discovery_product_features_config.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution_ess/server/product_features/cases_product_features_config.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution_ess/server/product_features/notes_product_features_config.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution_ess/server/product_features/security_product_features_config.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution_ess/server/product_features/siem_migrations_product_features_config.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution_ess/server/product_features/timeline_product_features_config.ts rename x-pack/solutions/security/plugins/security_solution_ess/server/product_features/{security_product_features_config.test.ts => update_global_artifact_replacements.test.ts} (88%) create mode 100644 x-pack/solutions/security/plugins/security_solution_ess/server/product_features/update_global_artifact_replacements.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/assistant_product_features_config.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/attack_discovery_product_features_config.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/cases_product_features_config.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/notes_product_features_config.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/security_product_features_config.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/siem_migrations_product_features_config.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/timeline_product_features_config.ts rename x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/{security_product_features_config.test.ts => update_global_artifact_replacements.test.ts} (98%) create mode 100644 x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/update_global_artifact_replacements.ts 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 272818315a9b6..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 @@ -8,17 +8,7 @@ import { AssistantSubFeatureId, ProductFeatureAssistantKey } from '../product_features_keys'; 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: ProductFeaturesConfig< +export const assistantProductFeaturesConfig: ProductFeaturesConfig< ProductFeatureAssistantKey, AssistantSubFeatureId > = { 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..322aba691029a 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,13 @@ 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(), + baseKibanaSubFeatureIds: [], + subFeaturesMap: new Map(), + 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 dafe8ba945761..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 @@ -8,17 +8,7 @@ import { ProductFeatureAttackDiscoveryKey } from '../product_features_keys'; 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: ProductFeaturesConfig = +export const attackDiscoveryProductFeaturesConfig: ProductFeaturesConfig = { [ProductFeatureAttackDiscoveryKey.attackDiscovery]: { privileges: { 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..b3571bfb26180 100644 --- a/x-pack/solutions/security/packages/features/src/cases/index.ts +++ b/x-pack/solutions/security/packages/features/src/cases/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 { 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 { @@ -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), + 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/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 803ac3fbe24e8..05ac8a182841b 100644 --- a/x-pack/solutions/security/packages/features/src/cases/types.ts +++ b/x-pack/solutions/security/packages/features/src/cases/types.ts @@ -9,12 +9,18 @@ import type { ProductFeatureCasesKey, CasesSubFeatureId } from '../product_featu 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 = ProductFeaturesConfig< +export type CasesProductFeaturesConfig = ProductFeaturesConfig< ProductFeatureCasesKey, 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..86aa40f719fd1 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 @@ -20,14 +20,9 @@ export const getCasesBaseKibanaSubFeatureIds = (): CasesSubFeatureId[] => [ CasesSubFeatureId.casesSettings, ]; -/** - * @deprecated Use getCasesSubFeaturesMapV2 instead - * @description - Defines all the Security Solution Cases available. - * The order of the subFeatures is the order they will be displayed - */ export const getCasesSubFeaturesMap = ({ - uiCapabilities, apiTags, + uiCapabilities, savedObjects, }: CasesFeatureParams) => { const deleteCasesSubFeature: SubFeatureConfig = { @@ -39,7 +34,7 @@ export const getCasesSubFeaturesMap = ({ groupType: 'independent', privileges: [ { - api: apiTags.delete, + api: apiTags.default.delete, id: 'cases_delete', name: i18n.translate( 'securitySolutionPackages.features.featureRegistry.deleteSubFeatureDetails', @@ -55,7 +50,7 @@ export const getCasesSubFeaturesMap = ({ cases: { delete: [APP_ID], }, - ui: uiCapabilities.delete, + ui: uiCapabilities.default.delete, replacedBy: [{ feature: CASES_FEATURE_ID_V3, privileges: ['cases_delete'] }], }, ], @@ -90,7 +85,7 @@ export const getCasesSubFeaturesMap = ({ cases: { settings: [APP_ID], }, - ui: uiCapabilities.settings, + ui: uiCapabilities.default.settings, replacedBy: [{ feature: CASES_FEATURE_ID_V3, privileges: ['cases_settings'] }], }, ], 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..3b919c0ab13df 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 @@ -27,8 +27,8 @@ export const getCasesBaseKibanaSubFeatureIdsV2 = (): CasesSubFeatureId[] => [ * The order of the subFeatures is the order they will be displayed */ export const getCasesSubFeaturesMapV2 = ({ - uiCapabilities, apiTags, + uiCapabilities, savedObjects, }: CasesFeatureParams) => { const deleteCasesSubFeature: SubFeatureConfig = { @@ -40,7 +40,7 @@ export const getCasesSubFeaturesMapV2 = ({ groupType: 'independent', privileges: [ { - api: apiTags.delete, + api: apiTags.default.delete, id: 'cases_delete', name: i18n.translate( 'securitySolutionPackages.features.featureRegistry.deleteSubFeatureDetails', @@ -56,7 +56,7 @@ export const getCasesSubFeaturesMapV2 = ({ cases: { delete: [APP_ID], }, - ui: uiCapabilities.delete, + ui: uiCapabilities.default.delete, replacedBy: [{ feature: CASES_FEATURE_ID_V3, privileges: ['cases_delete'] }], }, ], @@ -91,7 +91,7 @@ export const getCasesSubFeaturesMapV2 = ({ cases: { settings: [APP_ID], }, - ui: uiCapabilities.settings, + ui: uiCapabilities.default.settings, replacedBy: [{ feature: CASES_FEATURE_ID_V3, privileges: ['cases_settings'] }], }, ], @@ -113,7 +113,7 @@ export const getCasesSubFeaturesMapV2 = ({ groupType: 'independent', privileges: [ { - api: apiTags.createComment, + api: apiTags.default.createComment, id: 'create_comment', name: i18n.translate( 'securitySolutionPackages.features.featureRegistry.addCommentsSubFeatureDetails', @@ -129,7 +129,7 @@ export const getCasesSubFeaturesMapV2 = ({ cases: { createComment: [APP_ID], }, - ui: uiCapabilities.createComment, + ui: uiCapabilities.default.createComment, replacedBy: [{ feature: CASES_FEATURE_ID_V3, privileges: ['create_comment'] }], }, ], @@ -163,7 +163,7 @@ export const getCasesSubFeaturesMapV2 = ({ cases: { reopenCase: [APP_ID], }, - ui: uiCapabilities.reopenCase, + ui: uiCapabilities.default.reopenCase, replacedBy: [{ feature: CASES_FEATURE_ID_V3, privileges: ['case_reopen'] }], }, ], 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..3abf45354a073 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 @@ -28,8 +28,8 @@ export const getCasesBaseKibanaSubFeatureIdsV3 = (): CasesSubFeatureId[] => [ * The order of the subFeatures is the order they will be displayed */ export const getCasesSubFeaturesMapV3 = ({ - uiCapabilities, apiTags, + uiCapabilities, savedObjects, }: CasesFeatureParams) => { const deleteCasesSubFeature: SubFeatureConfig = { @@ -41,7 +41,7 @@ export const getCasesSubFeaturesMapV3 = ({ groupType: 'independent', privileges: [ { - api: apiTags.delete, + api: apiTags.default.delete, id: 'cases_delete', name: i18n.translate( 'securitySolutionPackages.features.featureRegistry.deleteSubFeatureDetails', @@ -57,7 +57,7 @@ export const getCasesSubFeaturesMapV3 = ({ cases: { delete: [APP_ID], }, - ui: uiCapabilities.delete, + ui: uiCapabilities.default.delete, }, ], }, @@ -91,7 +91,7 @@ export const getCasesSubFeaturesMapV3 = ({ cases: { settings: [APP_ID], }, - ui: uiCapabilities.settings, + ui: uiCapabilities.default.settings, }, ], }, @@ -110,7 +110,7 @@ export const getCasesSubFeaturesMapV3 = ({ groupType: 'independent', privileges: [ { - api: apiTags.createComment, + api: apiTags.default.createComment, id: 'create_comment', name: i18n.translate( 'securitySolutionPackages.features.featureRegistry.addCommentsSubFeatureDetails', @@ -126,7 +126,7 @@ export const getCasesSubFeaturesMapV3 = ({ cases: { createComment: [APP_ID], }, - ui: uiCapabilities.createComment, + ui: uiCapabilities.default.createComment, }, ], }, @@ -159,7 +159,7 @@ export const getCasesSubFeaturesMapV3 = ({ cases: { reopenCase: [APP_ID], }, - ui: uiCapabilities.reopenCase, + ui: uiCapabilities.default.reopenCase, }, ], }, @@ -187,7 +187,7 @@ export const getCasesSubFeaturesMapV3 = ({ cases: { assign: [APP_ID], }, - ui: uiCapabilities.assignCase, + ui: uiCapabilities.default.assignCase, }, ], }, diff --git a/x-pack/solutions/security/packages/features/src/helpers.test.ts b/x-pack/solutions/security/packages/features/src/helpers.test.ts index d0545efcf6e83..852eb38129795 100644 --- a/x-pack/solutions/security/packages/features/src/helpers.test.ts +++ b/x-pack/solutions/security/packages/features/src/helpers.test.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { createEnabledProductFeaturesConfigMap } from './helpers'; +import { createEnabledProductFeaturesConfigMap } from './tools'; import { ProductFeatureSecurityKey } from './product_features_keys'; import type { ProductFeatureKibanaConfig, ProductFeatureKeys } from './types'; 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 078eeac5bd303..0000000000000 --- a/x-pack/solutions/security/packages/features/src/helpers.ts +++ /dev/null @@ -1,39 +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. - * - * @param productFeatureKeys - The specific ProductFeatureKey enum e.g. ProductFeatureSecurityKey - * @param productFeaturesConfigs - The product features configs object, no need to include all keys, only the ones that have a config - * @param enabledProductFeaturesKeys - The enabled product features keys - * @returns A Map of all the enabled product features configs - */ -export const createEnabledProductFeaturesConfigMap = < - K extends ProductFeatureKeyType, - T extends string = string ->( - productFeatureKeys: Record, - productFeaturesConfigs: Partial>>, - enabledProductFeaturesKeys: ProductFeatureKeys -) => { - const allProductFeatureKeys = Object.values(productFeatureKeys) as K[]; - return new Map( - allProductFeatureKeys.reduce]>>((acc, key) => { - if (enabledProductFeaturesKeys.includes(key)) { - acc.push([key, productFeaturesConfigs[key] ?? {}]); - } - 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..97deacedb07b6 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,11 @@ 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 bdff573f1cd65..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 @@ -8,17 +8,7 @@ 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: ProductFeaturesConfig = { +export const notesProductFeaturesConfig: ProductFeaturesConfig = { [ProductFeatureNotesKey.notes]: { privileges: { all: { 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/product_feature_config.ts b/x-pack/solutions/security/packages/features/src/security/product_feature_config.ts index 9df712063b6de..c088e10e69353 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 @@ -9,17 +9,6 @@ import { ProductFeatureSecurityKey, SecuritySubFeatureId } from '../product_feat import { APP_ID } from '../constants'; 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: SecurityProductFeaturesConfig = { [ProductFeatureSecurityKey.advancedInsights]: { privileges: { @@ -164,4 +153,14 @@ export const securityDefaultProductFeaturesConfig: SecurityProductFeaturesConfig [ProductFeatureSecurityKey.securityWorkflowInsights]: { subFeatureIds: [SecuritySubFeatureId.workflowInsights], }, + + [ProductFeatureSecurityKey.endpointArtifactManagement]: { + subFeatureIds: [ + SecuritySubFeatureId.hostIsolationExceptionsBasic, + SecuritySubFeatureId.trustedApplications, + SecuritySubFeatureId.blocklist, + SecuritySubFeatureId.eventFilters, + SecuritySubFeatureId.globalArtifactManagement, + ], + }, }; 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..17f356b47277b --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/security/v1_features/product_feature_config.ts @@ -0,0 +1,23 @@ +/* + * 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 '../../tools'; +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]: { + privileges: { all: { api: [`${APP_ID}-writeGlobalArtifacts`] } }, + }, + }); 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..4f7ff6c6153d3 --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/security/v2_features/product_feature_config.ts @@ -0,0 +1,23 @@ +/* + * 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 '../../tools'; +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]: { + privileges: { all: { api: [`${APP_ID}-writeGlobalArtifacts`] } }, + }, + }); 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..f2cc84996d252 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,11 @@ 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 7487b935cbe95..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 @@ -9,17 +9,7 @@ import { SIEM_MIGRATIONS_API_ACTION_ALL } from '../actions'; import { ProductFeatureSiemMigrationsKey } from '../product_features_keys'; 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: ProductFeaturesConfig = +export const siemMigrationsProductFeaturesConfig: ProductFeaturesConfig = { [ProductFeatureSiemMigrationsKey.siemMigrations]: { privileges: { 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..39693820988e7 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,11 @@ 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 8b56d51a312df..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 @@ -8,28 +8,17 @@ 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: ProductFeaturesConfig = - { - [ProductFeatureTimelineKey.timeline]: { - privileges: { - all: { - api: ['timeline_read', 'timeline_write'], - ui: ['read', 'crud'], - }, - read: { - api: ['timeline_read'], - ui: ['read'], - }, +export const timelineProductFeaturesConfig: ProductFeaturesConfig = { + [ProductFeatureTimelineKey.timeline]: { + privileges: { + all: { + api: ['timeline_read', 'timeline_write'], + ui: ['read', 'crud'], + }, + read: { + api: ['timeline_read'], + ui: ['read'], }, }, - }; + }, +}; diff --git a/x-pack/solutions/security/packages/features/src/tools.ts b/x-pack/solutions/security/packages/features/src/tools.ts new file mode 100644 index 0000000000000..e6418a7a9b96d --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/tools.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mergeWith } from 'lodash'; +import type { ProductFeatureKeyType, ProductFeaturesConfig } from './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, (objValue: unknown, srcValue: unknown) => { + if (Array.isArray(objValue) && Array.isArray(srcValue)) { + return [...new Set([...objValue, ...srcValue])]; + } + return undefined; // Use default merge behavior for other types + }); +}; diff --git a/x-pack/solutions/security/packages/features/src/types.ts b/x-pack/solutions/security/packages/features/src/types.ts index 9bcce20138068..e63a76a92e3e8 100644 --- a/x-pack/solutions/security/packages/features/src/types.ts +++ b/x-pack/solutions/security/packages/features/src/types.ts @@ -60,63 +60,75 @@ export type ProductFeatureKibanaConfig = featureConfigModifier?: FeatureConfigModifier; }; +/** + * 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. + * - `featureConfigModifier`: a function to apply free modifications to the resulting Kibana feature config when a specific ProductFeatureKey is enabled. + */ export type ProductFeaturesConfig< - K extends ProductFeatureKeyType, + K extends ProductFeatureKeyType = ProductFeatureKeyType, T extends string = string > = Partial>>; -export type ProductFeaturesConfigMap = Map< - ProductFeatureKeyType, - ProductFeatureKibanaConfig ->; - -export type SecurityProductFeaturesConfigMap = Map< +export type SecurityProductFeaturesConfig = ProductFeaturesConfig< ProductFeatureSecurityKey, - ProductFeatureKibanaConfig + SecuritySubFeatureId >; -export type CasesProductFeaturesConfigMap = Map< +export type CasesProductFeaturesConfig = ProductFeaturesConfig< ProductFeatureCasesKey, - ProductFeatureKibanaConfig + CasesSubFeatureId >; - -export type AssistantProductFeaturesConfigMap = Map< +export type AssistantProductFeaturesConfig = ProductFeaturesConfig< ProductFeatureAssistantKey, - ProductFeatureKibanaConfig ->; - -export type AttackDiscoveryProductFeaturesConfigMap = Map< - ProductFeatureAttackDiscoveryKey, - ProductFeatureKibanaConfig ->; - -export type TimelineProductFeaturesConfigMap = Map< - ProductFeatureTimelineKey, - ProductFeatureKibanaConfig ->; - -export type NotesProductFeaturesConfigMap = Map; - -export type SiemMigrationsProductFeaturesConfigMap = Map< - ProductFeatureSiemMigrationsKey, - ProductFeatureKibanaConfig + AssistantSubFeatureId >; +export type AttackDiscoveryProductFeaturesConfig = + ProductFeaturesConfig; +export type TimelineProductFeaturesConfig = ProductFeaturesConfig; +export type NotesProductFeaturesConfig = ProductFeaturesConfig; +export type SiemMigrationsProductFeaturesConfig = + ProductFeaturesConfig; export type AppSubFeaturesMap = Map; -export interface ProductFeatureParams { +export interface ProductFeatureParams< + K extends ProductFeatureKeyType = ProductFeatureKeyType, + S extends string = string +> { baseKibanaFeature: BaseKibanaFeatureConfig; - baseKibanaSubFeatureIds: T[]; - subFeaturesMap: AppSubFeaturesMap; + baseKibanaSubFeatureIds: S[]; + subFeaturesMap: AppSubFeaturesMap; + productFeatureConfig: ProductFeaturesConfig; } +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; +} + +interface ProductFeatureConfigExtensions { + security: ConfigExtensions; + cases: ConfigExtensions; + securityAssistant: ConfigExtensions; + attackDiscovery: ConfigExtensions; + timeline: ConfigExtensions; + notes: ConfigExtensions; + siemMigrations: ConfigExtensions; +} + +export type ProductFeaturesConfiguratorExtensions = Partial; + export interface ProductFeaturesConfigurator { - security: () => SecurityProductFeaturesConfigMap; - cases: () => CasesProductFeaturesConfigMap; - securityAssistant: () => AssistantProductFeaturesConfigMap; - attackDiscovery: () => AttackDiscoveryProductFeaturesConfigMap; - timeline: () => TimelineProductFeaturesConfigMap; - notes: () => NotesProductFeaturesConfigMap; - siemMigrations: () => SiemMigrationsProductFeaturesConfigMap; + enabledProductFeatureKeys: ProductFeatureKeyType[]; + extensions?: ProductFeaturesConfiguratorExtensions; } -export type ProductFeatureGroup = keyof ProductFeaturesConfigurator; +export type ProductFeatureGroup = keyof ProductFeatureConfigExtensions; diff --git a/x-pack/solutions/security/packages/features/tools.ts b/x-pack/solutions/security/packages/features/tools.ts new file mode 100644 index 0000000000000..ebc0523e89f05 --- /dev/null +++ b/x-pack/solutions/security/packages/features/tools.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/tools'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/cases_product_feature_params.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/cases_product_feature_params.ts index 1e327d5c27266..fc750faed5c60 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/cases_product_feature_params.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/cases_product_feature_params.ts @@ -14,13 +14,13 @@ import { 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); -const casesUiCapabilities = { +const defaultUiCapabilities = { ...originalCasesUiCapabilities, all: originalCasesUiCapabilities.all.filter( (capability) => capability !== CASES_CONNECTORS_CAPABILITY @@ -30,7 +30,7 @@ const casesUiCapabilities = { ), }; -const casesApiTags = { +const defaultApiTags = { ...originalCasesApiTags, all: originalCasesApiTags.all.filter( (capability) => capability !== GET_CONNECTORS_CONFIGURE_API_TAG @@ -40,8 +40,23 @@ const casesApiTags = { ), }; -export const casesProductFeatureParams = { - uiCapabilities: casesUiCapabilities, - apiTags: casesApiTags, +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/product_features.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features.ts index db707b2e0f2d1..0e4c3cbbd7e01 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,58 +12,81 @@ import type { FeaturesPluginSetup, } from '@kbn/features-plugin/server'; import type { - ProductFeaturesConfigMap, ProductFeatureParams, ProductFeatureGroup, + ProductFeatureKeyType, + ProductFeaturesConfiguratorExtensions, + ProductFeatureKibanaConfig, } from '@kbn/security-solution-features'; +import { extendProductFeatureConfigs } from '@kbn/security-solution-features/tools'; import { ProductFeaturesConfigMerger } from './product_features_config_merger'; export class ProductFeatures { private featuresSetup?: FeaturesPluginSetup; - private readonly productFeatures: Map; + private readonly groupVersions: Map; private readonly registeredActions: Set; constructor(private readonly logger: Logger) { - this.productFeatures = new Map(); + this.groupVersions = new Map(); this.registeredActions = new Set(); } - public create( - featureGroup: ProductFeatureGroup, - params: Array> - ) { - this.productFeatures.set(featureGroup, params); + public create(featureGroup: ProductFeatureGroup, versions: ProductFeatureParams[]) { + this.groupVersions.set(featureGroup, versions); } public init(featuresSetup: FeaturesPluginSetup) { this.featuresSetup = featuresSetup; } - public register( - featureGroup: ProductFeatureGroup, - productFeatureConfig: ProductFeaturesConfigMap + public register( + enabledProductFeatureKeys: ProductFeatureKeyType[], + extensions: ProductFeaturesConfiguratorExtensions = {} ) { if (this.featuresSetup == null) { throw new Error('Cannot register product features. Service not initialized.'); } - const productFeaturesGroup = this.productFeatures.get(featureGroup); - if (!productFeaturesGroup) { - throw new Error(`No product feature found for group: ${featureGroup}`); - } - for (const params of productFeaturesGroup) { - const { baseKibanaFeature, baseKibanaSubFeatureIds, subFeaturesMap } = params; + 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] ?? {}; + + 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, subFeaturesMap); + const featureConfigMerger = new ProductFeaturesConfigMerger( + this.logger, + featureVersion.subFeaturesMap + ); - const completeProductFeatureConfig = featureConfigMerger.mergeProductFeatureConfigs( - baseKibanaFeature, - baseKibanaSubFeatureIds, - Array.from(productFeatureConfig.values()) - ); + const completeProductFeatureConfig = featureConfigMerger.mergeProductFeatureConfigs( + featureVersion.baseKibanaFeature, + featureVersion.baseKibanaSubFeatureIds, + filteredConfig + ); - this.featuresSetup.registerKibanaFeature(completeProductFeatureConfig); - this.addRegisteredActions(completeProductFeatureConfig); + this.featuresSetup.registerKibanaFeature(completeProductFeatureConfig); + this.addRegisteredActions(completeProductFeatureConfig); + } } } 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 a1a2c16b6385e..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 @@ -7,7 +7,6 @@ import type { Logger } from '@kbn/core/server'; import type { - ProductFeatureGroup, ProductFeatureKeyType, ProductFeaturesConfigurator, } from '@kbn/security-solution-features'; @@ -45,7 +44,7 @@ export class ProductFeaturesService { private productFeaturesRegistry: ProductFeatures; private enabledProductFeatures?: Set; - constructor(loggerFactory: Logger, private readonly experimentalFeatures: ExperimentalFeatures) { + constructor(loggerFactory: Logger, experimentalFeatures: ExperimentalFeatures) { this.logger = loggerFactory.get('productFeaturesService'); this.productFeaturesRegistry = new ProductFeatures(this.logger); @@ -61,7 +60,7 @@ export class ProductFeaturesService { getCasesV3Feature(casesProductFeatureParams), ]); this.productFeaturesRegistry.create('securityAssistant', [ - getAssistantFeature(this.experimentalFeatures), + getAssistantFeature(experimentalFeatures), ]); this.productFeaturesRegistry.create('attackDiscovery', [getAttackDiscoveryFeature()]); this.productFeaturesRegistry.create('timeline', [ @@ -70,7 +69,9 @@ export class ProductFeaturesService { this.productFeaturesRegistry.create('notes', [ getNotesFeature({ ...securityFeatureParams, savedObjects: securityNotesSavedObjects }), ]); - this.productFeaturesRegistry.create('siemMigrations', [getSiemMigrationsFeature()]); + if (!experimentalFeatures.siemMigrationsDisabled) { + this.productFeaturesRegistry.create('siemMigrations', [getSiemMigrationsFeature()]); + } } /** Initializes the features plugin setup */ @@ -84,16 +85,12 @@ export class ProductFeaturesService { /** Merges configurations of all the product features and registers them as Kibana features */ public setProductFeaturesConfigurator(configurator: ProductFeaturesConfigurator) { - const enabledProductFeatures: ProductFeatureKeyType[] = []; + const { enabledProductFeatureKeys, extensions } = configurator; + this.logger.debug(`Registering product features: ${enabledProductFeatureKeys.join(', ')}`); - for (const featureGroup of Object.keys(configurator) as ProductFeatureGroup[]) { - const productFeatureConfig = configurator[featureGroup](); - this.productFeaturesRegistry.register(featureGroup, productFeatureConfig); - enabledProductFeatures.push(...productFeatureConfig.keys()); - } - this.logger.debug(`Enabled product features: ${enabledProductFeatures.join(', ')}`); + this.productFeaturesRegistry.register(enabledProductFeatureKeys, extensions); - this.enabledProductFeatures = new Set(enabledProductFeatures); + this.enabledProductFeatures = new Set(enabledProductFeatureKeys); } /** Function to check if a specific product feature key is enabled */ 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..ca2ef888b0f11 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 { productFeaturesExtensions } from './product_features'; import { DEFAULT_PRODUCT_FEATURES } from '../common/constants'; import type { @@ -26,9 +26,10 @@ export class SecuritySolutionEssPlugin > { public setup(_coreSetup: CoreSetup, pluginsSetup: SecuritySolutionEssPluginSetupDeps) { - const productFeaturesConfigurator = - getProductProductFeaturesConfigurator(DEFAULT_PRODUCT_FEATURES); - pluginsSetup.securitySolution.setProductFeaturesConfigurator(productFeaturesConfigurator); + pluginsSetup.securitySolution.setProductFeaturesConfigurator({ + enabledProductFeatureKeys: DEFAULT_PRODUCT_FEATURES, + extensions: productFeaturesExtensions, + }); 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 e7f283b67a9a3..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/assistant_product_features_config.ts +++ /dev/null @@ -1,24 +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, - AssistantProductFeaturesConfigMap, -} from '@kbn/security-solution-features'; -import { - assistantDefaultProductFeaturesConfig, - createEnabledProductFeaturesConfigMap, -} from '@kbn/security-solution-features/config'; -import { ProductFeatureAssistantKey } from '@kbn/security-solution-features/keys'; - -export const getSecurityAssistantProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): AssistantProductFeaturesConfigMap => { - return createEnabledProductFeaturesConfigMap( - ProductFeatureAssistantKey, - assistantDefaultProductFeaturesConfig, - enabledProductFeatureKeys - ); - }; 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 86df2ade56e51..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/attack_discovery_product_features_config.ts +++ /dev/null @@ -1,24 +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, - AttackDiscoveryProductFeaturesConfigMap, -} from '@kbn/security-solution-features'; -import { - attackDiscoveryDefaultProductFeaturesConfig, - createEnabledProductFeaturesConfigMap, -} from '@kbn/security-solution-features/config'; -import { ProductFeatureAttackDiscoveryKey } from '@kbn/security-solution-features/keys'; - -export const getAttackDiscoveryProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): AttackDiscoveryProductFeaturesConfigMap => - createEnabledProductFeaturesConfigMap( - ProductFeatureAttackDiscoveryKey, - attackDiscoveryDefaultProductFeaturesConfig, - 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 974cee22ab4f7..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/cases_product_features_config.ts +++ /dev/null @@ -1,34 +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 { - CasesProductFeaturesConfigMap, - ProductFeatureKeys, -} from '@kbn/security-solution-features'; -import { - getCasesDefaultProductFeaturesConfig, - createEnabledProductFeaturesConfigMap, -} from '@kbn/security-solution-features/config'; - -import { - CASES_CONNECTORS_CAPABILITY, - GET_CONNECTORS_CONFIGURE_API_TAG, -} from '@kbn/cases-plugin/common/constants'; -import { ProductFeatureCasesKey } from '@kbn/security-solution-features/keys'; - -const casesProductFeaturesConfig = getCasesDefaultProductFeaturesConfig({ - apiTags: { connectors: GET_CONNECTORS_CONFIGURE_API_TAG }, - uiCapabilities: { connectors: CASES_CONNECTORS_CAPABILITY }, -}); - -export const getCasesProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): CasesProductFeaturesConfigMap => { - return createEnabledProductFeaturesConfigMap( - ProductFeatureCasesKey, - casesProductFeaturesConfig, - enabledProductFeatureKeys - ); - }; 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..631ef5251126f 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,38 @@ * 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 { ProductFeaturesConfiguratorExtensions } from '@kbn/security-solution-features'; +import { ProductFeatureSecurityKey } from '@kbn/security-solution-features/keys'; +import { APP_ID } from '@kbn/security-solution-plugin/common'; +import { updateGlobalArtifactManageReplacements } from './update_global_artifact_replacements'; -export const getProductProductFeaturesConfigurator = ( - 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), - }; +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]: { + featureConfigModifier: updateGlobalArtifactManageReplacements, + }, + }, + siemV2: { + [ProductFeatureSecurityKey.endpointArtifactManagement]: { + featureConfigModifier: updateGlobalArtifactManageReplacements, + }, + }, + }, + }, }; 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 fec45a186869e..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/notes_product_features_config.ts +++ /dev/null @@ -1,23 +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, - NotesProductFeaturesConfigMap, -} from '@kbn/security-solution-features'; -import { - notesDefaultProductFeaturesConfig, - createEnabledProductFeaturesConfigMap, -} from '@kbn/security-solution-features/config'; -import { ProductFeatureNotesKey } from '@kbn/security-solution-features/keys'; - -export const getNotesProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): NotesProductFeaturesConfigMap => - createEnabledProductFeaturesConfigMap( - ProductFeatureNotesKey, - notesDefaultProductFeaturesConfig, - enabledProductFeatureKeys - ); 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 769ee9b6e81ce..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/security_product_features_config.ts +++ /dev/null @@ -1,101 +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 { - FeatureConfigModifier, - ProductFeatureKeys, - SecurityProductFeaturesConfigMap, -} 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'; -import type { SecurityProductFeaturesConfig } from '@kbn/security-solution-features/src/security/types'; - -export const getSecurityProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): SecurityProductFeaturesConfigMap => { - return createEnabledProductFeaturesConfigMap( - ProductFeatureSecurityKey, - getSecurityEssProductFeaturesConfig(), - enabledProductFeatureKeys - ); - }; - -const getSecurityEssProductFeaturesConfig = (): SecurityProductFeaturesConfig => ({ - ...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, - ], - - featureConfigModifier: (featureConfig) => { - // When endpointArtifactManagement PLI is enabled, the replacedBy to the siemV3 feature needs to - // account for the privileges of the sub-features that are introduced by it. - updateGlobalArtifactManagerPrivileges(featureConfig); - }, - }, -}); - -export const updateGlobalArtifactManagerPrivileges: FeatureConfigModifier = (featureConfig) => { - if (!['siem', 'siemV2'].includes(featureConfig.id)) { - return; - } - - 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 - ]; - } - } - - // Add the global artifact management API privilege to the all privileges of siem and siemV2 features for backwards compatibility - // No need to add the ui capability since they are automatically added by the Kibana features framework via the `replacedBy` field. - featureConfig.privileges?.all.api?.push(`${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 0fe95462dff38..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/siem_migrations_product_features_config.ts +++ /dev/null @@ -1,24 +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, - SiemMigrationsProductFeaturesConfigMap, -} from '@kbn/security-solution-features'; -import { - siemMigrationsDefaultProductFeaturesConfig, - createEnabledProductFeaturesConfigMap, -} from '@kbn/security-solution-features/config'; -import { ProductFeatureSiemMigrationsKey } from '@kbn/security-solution-features/keys'; - -export const getSiemMigrationsProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): SiemMigrationsProductFeaturesConfigMap => - createEnabledProductFeaturesConfigMap( - ProductFeatureSiemMigrationsKey, - siemMigrationsDefaultProductFeaturesConfig, - 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 ed1c8d555ea22..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/timeline_product_features_config.ts +++ /dev/null @@ -1,24 +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, - TimelineProductFeaturesConfigMap, -} from '@kbn/security-solution-features'; -import { - createEnabledProductFeaturesConfigMap, - timelineDefaultProductFeaturesConfig, -} from '@kbn/security-solution-features/config'; -import { ProductFeatureTimelineKey } from '@kbn/security-solution-features/keys'; - -export const getTimelineProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): TimelineProductFeaturesConfigMap => - createEnabledProductFeaturesConfigMap( - ProductFeatureTimelineKey, - timelineDefaultProductFeaturesConfig, - enabledProductFeatureKeys - ); diff --git a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/security_product_features_config.test.ts b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/update_global_artifact_replacements.test.ts similarity index 88% rename from x-pack/solutions/security/plugins/security_solution_ess/server/product_features/security_product_features_config.test.ts rename to x-pack/solutions/security/plugins/security_solution_ess/server/product_features/update_global_artifact_replacements.test.ts index 983428cf8bf4e..01ee53522c8e7 100644 --- a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/security_product_features_config.test.ts +++ b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/update_global_artifact_replacements.test.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { updateGlobalArtifactManagerPrivileges } from './security_product_features_config'; +import { updateGlobalArtifactManageReplacements } from './update_global_artifact_replacements'; import { SECURITY_FEATURE_ID_V3 } from '@kbn/security-solution-features/constants'; import type { MutableKibanaFeatureConfig } from '@kbn/security-solution-features'; import { APP_ID } from '@kbn/security-solution-plugin/common'; @@ -35,7 +35,7 @@ const baseFeatureConfig: MutableKibanaFeatureConfig = { }, }; -describe('updateGlobalArtifactManagerPrivileges', () => { +describe('updateGlobalArtifactManageReplacements', () => { describe.each(['siem', 'siemV2'])('when features id is %s', (featureId: string) => { let featureConfig: MutableKibanaFeatureConfig; @@ -46,7 +46,7 @@ describe('updateGlobalArtifactManagerPrivileges', () => { it('should do nothing if replacedBy is not present', () => { const originalConfig = JSON.parse(JSON.stringify(featureConfig)); - updateGlobalArtifactManagerPrivileges(featureConfig as MutableKibanaFeatureConfig); + updateGlobalArtifactManageReplacements(featureConfig as MutableKibanaFeatureConfig); expect(featureConfig).toEqual(originalConfig); }); @@ -69,7 +69,7 @@ describe('updateGlobalArtifactManagerPrivileges', () => { }, }; - updateGlobalArtifactManagerPrivileges(testFeatureConfig as MutableKibanaFeatureConfig); + updateGlobalArtifactManageReplacements(testFeatureConfig as MutableKibanaFeatureConfig); const replacedBy = testFeatureConfig.privileges.all.replacedBy; @@ -107,7 +107,7 @@ describe('updateGlobalArtifactManagerPrivileges', () => { }, }; - updateGlobalArtifactManagerPrivileges(testFeatureConfig as MutableKibanaFeatureConfig); + updateGlobalArtifactManageReplacements(testFeatureConfig as MutableKibanaFeatureConfig); const replacedBy = testFeatureConfig.privileges.all.replacedBy; @@ -128,7 +128,7 @@ describe('updateGlobalArtifactManagerPrivileges', () => { }, }; - updateGlobalArtifactManagerPrivileges(testFeatureConfig as MutableKibanaFeatureConfig); + updateGlobalArtifactManageReplacements(testFeatureConfig as MutableKibanaFeatureConfig); expect(featureConfig.privileges?.all.api).toContain(`${APP_ID}-writeGlobalArtifacts`); }); @@ -151,7 +151,7 @@ describe('updateGlobalArtifactManagerPrivileges', () => { }, }; - updateGlobalArtifactManagerPrivileges(featureConfig as MutableKibanaFeatureConfig); + updateGlobalArtifactManageReplacements(featureConfig as MutableKibanaFeatureConfig); const replacedBy = featureConfig.privileges.all.replacedBy; @@ -163,7 +163,7 @@ describe('updateGlobalArtifactManagerPrivileges', () => { it('should not add writeGlobalArtifacts api action', () => { const featureConfig = { ...baseFeatureConfig, id: 'siemV3' }; - updateGlobalArtifactManagerPrivileges(featureConfig as MutableKibanaFeatureConfig); + updateGlobalArtifactManageReplacements(featureConfig as MutableKibanaFeatureConfig); expect(featureConfig.privileges?.all.api).not.toContain(`${APP_ID}-writeGlobalArtifacts`); }); diff --git a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/update_global_artifact_replacements.ts b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/update_global_artifact_replacements.ts new file mode 100644 index 0000000000000..e34792befe349 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/update_global_artifact_replacements.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { FeatureConfigModifier } from '@kbn/security-solution-features'; +import { SECURITY_FEATURE_ID_V3 } from '@kbn/security-solution-features/constants'; + +// When endpointArtifactManagement PLI is enabled, the replacedBy to the siemV3 feature needs to +// account for the privileges of the sub-features that are introduced by it. +// This needs to be done here because the replacements of serverless and ESS are different. +export const updateGlobalArtifactManageReplacements: FeatureConfigModifier = (featureConfig) => { + 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_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 e7f283b67a9a3..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/assistant_product_features_config.ts +++ /dev/null @@ -1,24 +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, - AssistantProductFeaturesConfigMap, -} from '@kbn/security-solution-features'; -import { - assistantDefaultProductFeaturesConfig, - createEnabledProductFeaturesConfigMap, -} from '@kbn/security-solution-features/config'; -import { ProductFeatureAssistantKey } from '@kbn/security-solution-features/keys'; - -export const getSecurityAssistantProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): AssistantProductFeaturesConfigMap => { - return createEnabledProductFeaturesConfigMap( - ProductFeatureAssistantKey, - assistantDefaultProductFeaturesConfig, - enabledProductFeatureKeys - ); - }; 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 acfadddbd16aa..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/attack_discovery_product_features_config.ts +++ /dev/null @@ -1,23 +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, - AttackDiscoveryProductFeaturesConfigMap, -} from '@kbn/security-solution-features'; -import { - attackDiscoveryDefaultProductFeaturesConfig, - createEnabledProductFeaturesConfigMap, -} from '@kbn/security-solution-features/config'; -import { ProductFeatureAttackDiscoveryKey } from '@kbn/security-solution-features/keys'; - -export const getAttackDiscoveryProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): AttackDiscoveryProductFeaturesConfigMap => - createEnabledProductFeaturesConfigMap( - ProductFeatureAttackDiscoveryKey, - attackDiscoveryDefaultProductFeaturesConfig, - 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 44026bec51659..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/cases_product_features_config.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 { - CasesProductFeaturesConfigMap, - ProductFeatureKeys, -} from '@kbn/security-solution-features'; -import { - getCasesDefaultProductFeaturesConfig, - createEnabledProductFeaturesConfigMap, -} from '@kbn/security-solution-features/config'; -import { - CASES_CONNECTORS_CAPABILITY, - GET_CONNECTORS_CONFIGURE_API_TAG, -} from '@kbn/cases-plugin/common/constants'; -import { ProductFeatureCasesKey } from '@kbn/security-solution-features/keys'; - -const casesProductFeaturesConfig = getCasesDefaultProductFeaturesConfig({ - apiTags: { connectors: GET_CONNECTORS_CONFIGURE_API_TAG }, - uiCapabilities: { connectors: CASES_CONNECTORS_CAPABILITY }, -}); - -export const getCasesProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): CasesProductFeaturesConfigMap => { - return createEnabledProductFeaturesConfigMap( - ProductFeatureCasesKey, - casesProductFeaturesConfig, - enabledProductFeatureKeys - ); - }; diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/index.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/index.ts index 43ba3a75edf72..073671970562d 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 @@ -7,19 +7,17 @@ import type { Logger } from '@kbn/logging'; -import { ProductFeatureKey } from '@kbn/security-solution-features/keys'; +import { + ProductFeatureKey, + ProductFeatureSecurityKey, + SecuritySubFeatureId, +} 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 { updateGlobalArtifactManagerPrivileges } from './update_global_artifact_replacements'; export const registerProductFeatures = ( pluginsSetup: SecuritySolutionServerlessPluginSetupDeps, @@ -36,16 +34,28 @@ 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: { + security: { + allVersions: { + [ProductFeatureSecurityKey.endpointExceptions]: { + subFeatureIds: [SecuritySubFeatureId.endpointExceptions], + }, + }, + version: { + siem: { + [ProductFeatureSecurityKey.endpointArtifactManagement]: { + featureConfigModifier: updateGlobalArtifactManagerPrivileges, + }, + }, + siemV2: { + [ProductFeatureSecurityKey.endpointArtifactManagement]: { + featureConfigModifier: updateGlobalArtifactManagerPrivileges, + }, + }, + }, + }, + }, }); // 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 fec45a186869e..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/notes_product_features_config.ts +++ /dev/null @@ -1,23 +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, - NotesProductFeaturesConfigMap, -} from '@kbn/security-solution-features'; -import { - notesDefaultProductFeaturesConfig, - createEnabledProductFeaturesConfigMap, -} from '@kbn/security-solution-features/config'; -import { ProductFeatureNotesKey } from '@kbn/security-solution-features/keys'; - -export const getNotesProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): NotesProductFeaturesConfigMap => - createEnabledProductFeaturesConfigMap( - ProductFeatureNotesKey, - notesDefaultProductFeaturesConfig, - enabledProductFeatureKeys - ); 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 3b0ca66a0f397..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/security_product_features_config.ts +++ /dev/null @@ -1,92 +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 { - FeatureConfigModifier, - ProductFeatureKeys, - SecurityProductFeaturesConfigMap, -} 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 { SecurityProductFeaturesConfig } from '@kbn/security-solution-features/src/security/types'; -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 - ) => - (): SecurityProductFeaturesConfigMap => { - return createEnabledProductFeaturesConfigMap( - ProductFeatureSecurityKey, - getSecurityProductFeaturesConfig(), - enabledProductFeatureKeys - ); - }; - -const getSecurityProductFeaturesConfig = (): SecurityProductFeaturesConfig => ({ - ...securityDefaultProductFeaturesConfig, - - [ProductFeatureSecurityKey.endpointExceptions]: { - subFeatureIds: [SecuritySubFeatureId.endpointExceptions], - }, - - [ProductFeatureSecurityKey.endpointArtifactManagement]: { - subFeatureIds: [ - SecuritySubFeatureId.hostIsolationExceptionsBasic, - SecuritySubFeatureId.trustedApplications, - SecuritySubFeatureId.blocklist, - SecuritySubFeatureId.eventFilters, - SecuritySubFeatureId.globalArtifactManagement, - ], - - featureConfigModifier: (featureConfig) => { - // When endpointArtifactManagement PLI is enabled, the replacedBy for the siemV3 feature needs to - // account for the privileges of the sub-features that are introduced by it. - updateGlobalArtifactManagerPrivileges(featureConfig); - }, - }, -}); - -export const updateGlobalArtifactManagerPrivileges: FeatureConfigModifier = (featureConfig) => { - if (!['siem', 'siemV2'].includes(featureConfig.id)) { - return; - } - - 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', - ]; - } - - // Add the global artifact management API privilege to the all privileges of siem and siemV2 features for backwards compatibility - // No need to add the ui capability since they are automatically added by the Kibana features framework via the `replacedBy` field. - featureConfig.privileges?.all.api?.push(`${APP_ID}-writeGlobalArtifacts`); -}; 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 7ab03f596044e..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/siem_migrations_product_features_config.ts +++ /dev/null @@ -1,23 +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, - SiemMigrationsProductFeaturesConfigMap, -} from '@kbn/security-solution-features'; -import { - siemMigrationsDefaultProductFeaturesConfig, - createEnabledProductFeaturesConfigMap, -} from '@kbn/security-solution-features/config'; -import { ProductFeatureSiemMigrationsKey } from '@kbn/security-solution-features/keys'; - -export const getSiemMigrationsProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): SiemMigrationsProductFeaturesConfigMap => - createEnabledProductFeaturesConfigMap( - ProductFeatureSiemMigrationsKey, - siemMigrationsDefaultProductFeaturesConfig, - 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 4e5691a928a3a..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/timeline_product_features_config.ts +++ /dev/null @@ -1,23 +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, - TimelineProductFeaturesConfigMap, -} from '@kbn/security-solution-features'; -import { - timelineDefaultProductFeaturesConfig, - createEnabledProductFeaturesConfigMap, -} from '@kbn/security-solution-features/config'; -import { ProductFeatureTimelineKey } from '@kbn/security-solution-features/keys'; - -export const getTimelineProductFeaturesConfigurator = - (enabledProductFeatureKeys: ProductFeatureKeys) => (): TimelineProductFeaturesConfigMap => - createEnabledProductFeaturesConfigMap( - ProductFeatureTimelineKey, - timelineDefaultProductFeaturesConfig, - enabledProductFeatureKeys - ); diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/security_product_features_config.test.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/update_global_artifact_replacements.test.ts similarity index 98% rename from x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/security_product_features_config.test.ts rename to x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/update_global_artifact_replacements.test.ts index aab02c52b4188..1140b3440c8ee 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/security_product_features_config.test.ts +++ b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/update_global_artifact_replacements.test.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { updateGlobalArtifactManagerPrivileges } from './security_product_features_config'; +import { updateGlobalArtifactManagerPrivileges } from './update_global_artifact_replacements'; import { SECURITY_FEATURE_ID_V3 } from '@kbn/security-solution-features/constants'; import type { MutableKibanaFeatureConfig } from '@kbn/security-solution-features'; import { APP_ID } from '@kbn/security-solution-plugin/common'; diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/update_global_artifact_replacements.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/update_global_artifact_replacements.ts new file mode 100644 index 0000000000000..8a3cb4d0f5ddb --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/update_global_artifact_replacements.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { FeatureConfigModifier } from '@kbn/security-solution-features'; +import { SECURITY_FEATURE_ID_V3 } from '@kbn/security-solution-features/constants'; + +// When endpointArtifactManagement PLI is enabled, the replacedBy to the siemV3 feature needs to +// account for the privileges of the sub-features that are introduced by it. +// This needs to be done here because the replacements of serverless and ESS are different. +export const updateGlobalArtifactManagerPrivileges: FeatureConfigModifier = (featureConfig) => { + 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', + ]; + } +}; From f75c9c01cddf742a1ab0bf43bbfecc363f6b31d3 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Thu, 31 Jul 2025 11:10:46 +0200 Subject: [PATCH 15/28] fix tests --- .../security_solution_ess/server/plugin.ts | 7 +- .../server/product_features/index.ts | 44 ++--- ...pdate_global_artifact_replacements.test.ts | 171 ------------------ .../update_global_artifact_replacements.ts | 40 ---- .../server/plugin.ts | 2 +- .../server/product_features/index.ts | 33 +--- ...pdate_global_artifact_replacements.test.ts | 169 ----------------- .../update_global_artifact_replacements.ts | 35 ---- 8 files changed, 18 insertions(+), 483 deletions(-) delete mode 100644 x-pack/solutions/security/plugins/security_solution_ess/server/product_features/update_global_artifact_replacements.test.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution_ess/server/product_features/update_global_artifact_replacements.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/update_global_artifact_replacements.test.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/update_global_artifact_replacements.ts 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 ca2ef888b0f11..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 { productFeaturesExtensions } from './product_features'; +import { registerProductFeatures } from './product_features'; import { DEFAULT_PRODUCT_FEATURES } from '../common/constants'; import type { @@ -26,10 +26,7 @@ export class SecuritySolutionEssPlugin > { public setup(_coreSetup: CoreSetup, pluginsSetup: SecuritySolutionEssPluginSetupDeps) { - pluginsSetup.securitySolution.setProductFeaturesConfigurator({ - enabledProductFeatureKeys: DEFAULT_PRODUCT_FEATURES, - extensions: productFeaturesExtensions, - }); + registerProductFeatures(pluginsSetup, DEFAULT_PRODUCT_FEATURES); return {}; } 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 631ef5251126f..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,38 +5,16 @@ * 2.0. */ -import type { ProductFeaturesConfiguratorExtensions } from '@kbn/security-solution-features'; -import { ProductFeatureSecurityKey } from '@kbn/security-solution-features/keys'; -import { APP_ID } from '@kbn/security-solution-plugin/common'; -import { updateGlobalArtifactManageReplacements } from './update_global_artifact_replacements'; +import type { ProductFeatureKeys } from '@kbn/security-solution-features'; +import type { SecuritySolutionEssPluginSetupDeps } from '../types'; +import { productFeaturesExtensions } from './product_features_extensions'; -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]: { - featureConfigModifier: updateGlobalArtifactManageReplacements, - }, - }, - siemV2: { - [ProductFeatureSecurityKey.endpointArtifactManagement]: { - featureConfigModifier: updateGlobalArtifactManageReplacements, - }, - }, - }, - }, +export const registerProductFeatures = ( + pluginsSetup: SecuritySolutionEssPluginSetupDeps, + enabledProductFeatureKeys: ProductFeatureKeys +): void => { + pluginsSetup.securitySolution.setProductFeaturesConfigurator({ + enabledProductFeatureKeys, + extensions: productFeaturesExtensions, + }); }; diff --git a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/update_global_artifact_replacements.test.ts b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/update_global_artifact_replacements.test.ts deleted file mode 100644 index 01ee53522c8e7..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/update_global_artifact_replacements.test.ts +++ /dev/null @@ -1,171 +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 { updateGlobalArtifactManageReplacements } from './update_global_artifact_replacements'; -import { SECURITY_FEATURE_ID_V3 } from '@kbn/security-solution-features/constants'; -import type { MutableKibanaFeatureConfig } from '@kbn/security-solution-features'; -import { APP_ID } from '@kbn/security-solution-plugin/common'; -import { cloneDeep } from 'lodash'; - -const baseFeatureConfig: MutableKibanaFeatureConfig = { - id: '[setTestFeatureId]', - 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', () => { - describe.each(['siem', 'siemV2'])('when features id is %s', (featureId: string) => { - let featureConfig: MutableKibanaFeatureConfig; - - beforeEach(() => { - featureConfig = { ...cloneDeep(baseFeatureConfig), id: featureId }; - }); - - 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']); - }); - - it('should add writeGlobalArtifacts api action', () => { - const testFeatureConfig = { - ...featureConfig, - privileges: { - ...featureConfig.privileges, - all: { - ...featureConfig.privileges?.all, - replacedBy: { default: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['all'] }] }, - }, - }, - }; - - updateGlobalArtifactManageReplacements(testFeatureConfig as MutableKibanaFeatureConfig); - - expect(featureConfig.privileges?.all.api).toContain(`${APP_ID}-writeGlobalArtifacts`); - }); - }); - - describe('when feature id is siemV3', () => { - it('should not modify privileges', () => { - const featureConfig = { - ...baseFeatureConfig, - id: 'siemV3', - privileges: { - ...baseFeatureConfig.privileges, - all: { - ...baseFeatureConfig.privileges?.all, - replacedBy: { - default: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['all'] }], - minimal: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['all'] }], - }, - }, - }, - }; - - updateGlobalArtifactManageReplacements(featureConfig as MutableKibanaFeatureConfig); - - const replacedBy = featureConfig.privileges.all.replacedBy; - - // No changes to SECURITY_FEATURE_ID_V3 - expect(replacedBy.default[0].privileges).toEqual(['all']); - expect(replacedBy.minimal[0].privileges).toEqual(['all']); - }); - - it('should not add writeGlobalArtifacts api action', () => { - const featureConfig = { ...baseFeatureConfig, id: 'siemV3' }; - - updateGlobalArtifactManageReplacements(featureConfig as MutableKibanaFeatureConfig); - - expect(featureConfig.privileges?.all.api).not.toContain(`${APP_ID}-writeGlobalArtifacts`); - }); - }); -}); diff --git a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/update_global_artifact_replacements.ts b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/update_global_artifact_replacements.ts deleted file mode 100644 index e34792befe349..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/update_global_artifact_replacements.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 { FeatureConfigModifier } from '@kbn/security-solution-features'; -import { SECURITY_FEATURE_ID_V3 } from '@kbn/security-solution-features/constants'; - -// When endpointArtifactManagement PLI is enabled, the replacedBy to the siemV3 feature needs to -// account for the privileges of the sub-features that are introduced by it. -// This needs to be done here because the replacements of serverless and ESS are different. -export const updateGlobalArtifactManageReplacements: FeatureConfigModifier = (featureConfig) => { - 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_serverless/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/plugin.ts index be9a1b14846ff..76e8b0b5a5e7a 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 @@ -82,7 +82,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/index.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/index.ts index 073671970562d..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 @@ -7,22 +7,17 @@ import type { Logger } from '@kbn/logging'; -import { - ProductFeatureKey, - ProductFeatureSecurityKey, - SecuritySubFeatureId, -} from '@kbn/security-solution-features/keys'; +import { ProductFeatureKey } from '@kbn/security-solution-features/keys'; import type { ProductFeatureKeys } from '@kbn/security-solution-features'; import { enableRuleActions } from '../rules/enable_rule_actions'; import type { ServerlessSecurityConfig } from '../config'; import type { Tier, SecuritySolutionServerlessPluginSetupDeps } from '../types'; import { ProductLine } from '../../common/product'; -import { updateGlobalArtifactManagerPrivileges } from './update_global_artifact_replacements'; +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 @@ -35,27 +30,7 @@ export const registerProductFeatures = ( // register product features for the main security solution product features service pluginsSetup.securitySolution.setProductFeaturesConfigurator({ enabledProductFeatureKeys, - extensions: { - security: { - allVersions: { - [ProductFeatureSecurityKey.endpointExceptions]: { - subFeatureIds: [SecuritySubFeatureId.endpointExceptions], - }, - }, - version: { - siem: { - [ProductFeatureSecurityKey.endpointArtifactManagement]: { - featureConfigModifier: updateGlobalArtifactManagerPrivileges, - }, - }, - siemV2: { - [ProductFeatureSecurityKey.endpointArtifactManagement]: { - featureConfigModifier: updateGlobalArtifactManagerPrivileges, - }, - }, - }, - }, - }, + 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/update_global_artifact_replacements.test.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/update_global_artifact_replacements.test.ts deleted file mode 100644 index 1140b3440c8ee..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/update_global_artifact_replacements.test.ts +++ /dev/null @@ -1,169 +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 { updateGlobalArtifactManagerPrivileges } from './update_global_artifact_replacements'; -import { SECURITY_FEATURE_ID_V3 } from '@kbn/security-solution-features/constants'; -import type { MutableKibanaFeatureConfig } from '@kbn/security-solution-features'; -import { APP_ID } from '@kbn/security-solution-plugin/common'; -import { cloneDeep } from 'lodash'; - -const baseFeatureConfig: MutableKibanaFeatureConfig = { - id: '[setTestFeatureId]', - 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('updateGlobalArtifactManagerPrivileges', () => { - describe.each(['siem', 'siemV2'])('when features id is %s', (featureId: string) => { - let featureConfig: MutableKibanaFeatureConfig; - - beforeEach(() => { - featureConfig = { ...cloneDeep(baseFeatureConfig), id: featureId }; - }); - - it('should do nothing if replacedBy is not present', () => { - const originalConfig = JSON.parse(JSON.stringify(featureConfig)); - - updateGlobalArtifactManagerPrivileges(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'] }], - }, - }, - }, - }; - - updateGlobalArtifactManagerPrivileges(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'] }], - }, - }, - }, - }; - - updateGlobalArtifactManagerPrivileges(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']); - }); - - it('should add writeGlobalArtifacts api action', () => { - const testFeatureConfig = { - ...featureConfig, - privileges: { - ...featureConfig.privileges, - all: { - ...featureConfig.privileges?.all, - replacedBy: { default: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['all'] }] }, - }, - }, - }; - - updateGlobalArtifactManagerPrivileges(testFeatureConfig as MutableKibanaFeatureConfig); - - expect(featureConfig.privileges?.all.api).toContain(`${APP_ID}-writeGlobalArtifacts`); - }); - }); - - describe('when feature id is siemV3', () => { - it('should not modify privileges', () => { - const featureConfig = { - ...baseFeatureConfig, - id: 'siemV3', - privileges: { - ...baseFeatureConfig.privileges, - all: { - ...baseFeatureConfig.privileges?.all, - replacedBy: { - default: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['all'] }], - minimal: [{ feature: SECURITY_FEATURE_ID_V3, privileges: ['all'] }], - }, - }, - }, - }; - - updateGlobalArtifactManagerPrivileges(featureConfig as MutableKibanaFeatureConfig); - - const replacedBy = featureConfig.privileges.all.replacedBy; - - // No changes to SECURITY_FEATURE_ID_V3 - expect(replacedBy.default[0].privileges).toEqual(['all']); - expect(replacedBy.minimal[0].privileges).toEqual(['all']); - }); - - it('should not add writeGlobalArtifacts api action', () => { - const featureConfig = { ...baseFeatureConfig, id: 'siemV3' }; - - updateGlobalArtifactManagerPrivileges(featureConfig as MutableKibanaFeatureConfig); - - expect(featureConfig.privileges?.all.api).not.toContain(`${APP_ID}-writeGlobalArtifacts`); - }); - }); -}); diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/update_global_artifact_replacements.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/update_global_artifact_replacements.ts deleted file mode 100644 index 8a3cb4d0f5ddb..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/update_global_artifact_replacements.ts +++ /dev/null @@ -1,35 +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 { FeatureConfigModifier } from '@kbn/security-solution-features'; -import { SECURITY_FEATURE_ID_V3 } from '@kbn/security-solution-features/constants'; - -// When endpointArtifactManagement PLI is enabled, the replacedBy to the siemV3 feature needs to -// account for the privileges of the sub-features that are introduced by it. -// This needs to be done here because the replacements of serverless and ESS are different. -export const updateGlobalArtifactManagerPrivileges: FeatureConfigModifier = (featureConfig) => { - 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', - ]; - } -}; From 5da13598b62a9e5e248c70a2b67dc82d9197502e Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Thu, 31 Jul 2025 14:11:50 +0200 Subject: [PATCH 16/28] unify sub-features version config --- .../features/src/attack_discovery/index.ts | 2 - .../packages/features/src/cases/index.ts | 8 +- .../features/src/cases/kibana_sub_features.ts | 175 ++++ .../cases/v1_features/kibana_sub_features.ts | 93 +- .../cases/v2_features/kibana_sub_features.ts | 168 +-- .../cases/v3_features/kibana_sub_features.ts | 192 +--- .../packages/features/src/notes/index.ts | 2 - .../src/security/kibana_sub_features.ts | 702 +++++++++++++ .../v1_features/kibana_sub_features.ts | 839 ++------------- .../v1_features/kibana_sub_features_old.ts | 811 +++++++++++++++ .../v2_features/kibana_sub_features.ts | 955 ++---------------- .../v2_features/kibana_sub_features_old.ts | 939 +++++++++++++++++ .../v3_features/kibana_sub_features.ts | 857 +--------------- .../features/src/siem_migrations/index.ts | 2 - .../packages/features/src/timeline/index.ts | 2 - .../security/packages/features/src/tools.ts | 64 +- .../security/packages/features/src/types.ts | 16 +- .../product_features.ts | 6 +- .../product_features_extensions.test.ts | 116 +++ .../product_features_extensions.ts | 78 ++ .../product_features_extensions.test.ts | 114 +++ .../product_features_extensions.ts | 66 ++ 22 files changed, 3334 insertions(+), 2873 deletions(-) create mode 100644 x-pack/solutions/security/packages/features/src/cases/kibana_sub_features.ts create mode 100644 x-pack/solutions/security/packages/features/src/security/kibana_sub_features.ts create mode 100644 x-pack/solutions/security/packages/features/src/security/v1_features/kibana_sub_features_old.ts create mode 100644 x-pack/solutions/security/packages/features/src/security/v2_features/kibana_sub_features_old.ts create mode 100644 x-pack/solutions/security/plugins/security_solution_ess/server/product_features/product_features_extensions.test.ts create mode 100644 x-pack/solutions/security/plugins/security_solution_ess/server/product_features/product_features_extensions.ts create mode 100644 x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/product_features_extensions.test.ts create mode 100644 x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/product_features_extensions.ts 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 322aba691029a..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 @@ -13,7 +13,5 @@ import type { ProductFeatureAttackDiscoveryKey } from '../product_features_keys' export const getAttackDiscoveryFeature = (): ProductFeatureParams => ({ baseKibanaFeature: getAttackDiscoveryBaseKibanaFeature(), - baseKibanaSubFeatureIds: [], - subFeaturesMap: new Map(), productFeatureConfig: attackDiscoveryProductFeaturesConfig, }); 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 b3571bfb26180..2b60a7bb9a5ae 100644 --- a/x-pack/solutions/security/packages/features/src/cases/index.ts +++ b/x-pack/solutions/security/packages/features/src/cases/index.ts @@ -8,8 +8,8 @@ import type { CasesSubFeatureId, ProductFeatureCasesKey } from '../product_featu 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'; @@ -28,8 +28,8 @@ export const getCasesFeature = ( params: CasesFeatureParams ): ProductFeatureParams => ({ baseKibanaFeature: getCasesBaseKibanaFeature(params), - baseKibanaSubFeatureIds: getCasesBaseKibanaSubFeatureIds(), - subFeaturesMap: getCasesSubFeaturesMap(params), + baseKibanaSubFeatureIds: getCasesBaseKibanaSubFeatureIdsV1(), + subFeaturesMap: getCasesSubFeaturesMapV1(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/v1_features/kibana_sub_features.ts b/x-pack/solutions/security/packages/features/src/cases/v1_features/kibana_sub_features.ts index 86aa40f719fd1..8f79541a8b17d 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,96 +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 '../../tools'; +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, ]; -export const getCasesSubFeaturesMap = ({ - apiTags, - uiCapabilities, - savedObjects, -}: CasesFeatureParams) => { - const deleteCasesSubFeature: 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, - 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.default.settings, - replacedBy: [{ feature: CASES_FEATURE_ID_V3, privileges: ['cases_settings'] }], - }, - ], - }, - ], - }; - - return new Map([ - [CasesSubFeatureId.deleteCases, deleteCasesSubFeature], - [CasesSubFeatureId.casesSettings, casesSettingsCasesSubFeature], +/** + * Defines all the Security Solution Cases subFeatures available. + * The order of the subFeatures is the order they will be displayed + */ +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/v2_features/kibana_sub_features.ts b/x-pack/solutions/security/packages/features/src/cases/v2_features/kibana_sub_features.ts index 3b919c0ab13df..2733150c332c5 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 '../../tools'; /** * 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 = ({ - apiTags, - uiCapabilities, - savedObjects, -}: CasesFeatureParams) => { - const deleteCasesSubFeature: 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, - 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.default.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.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, - 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.default.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_sub_features.ts b/x-pack/solutions/security/packages/features/src/cases/v3_features/kibana_sub_features.ts index 3abf45354a073..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 = ({ - apiTags, - uiCapabilities, - savedObjects, -}: CasesFeatureParams) => { - const deleteCasesSubFeature: 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, - }, - ], - }, - ], - }; - - 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.default.settings, - }, - ], - }, - ], - }; - - const casesAddCommentsCasesSubFeature: 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, - }, - ], - }, - ], - }; - 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.default.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.default.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/notes/index.ts b/x-pack/solutions/security/packages/features/src/notes/index.ts index 97deacedb07b6..525b75cac58a8 100644 --- a/x-pack/solutions/security/packages/features/src/notes/index.ts +++ b/x-pack/solutions/security/packages/features/src/notes/index.ts @@ -12,7 +12,5 @@ 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/security/kibana_sub_features.ts b/x-pack/solutions/security/packages/features/src/security/kibana_sub_features.ts new file mode 100644 index 0000000000000..cf3ad8037bd44 --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/security/kibana_sub_features.ts @@ -0,0 +1,702 @@ +/* + * 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 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/v1_features/kibana_sub_features.ts b/x-pack/solutions/security/packages/features/src/security/v1_features/kibana_sub_features.ts index 970078c392830..3ce3462b4bdbb 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'], - }, - ], - }, - ], -}); - -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: [ - { - 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'], - }, - ], - }, - ], -}); -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: [ - { - 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'], - }, - ], - }, - ], -}); -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: [ - { - 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'], - }, - ], - }, - ], -}); -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: [ - { - 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'], - }, - ], - }, - ], -}); -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'], - }, - ], +import type { SubFeatureReplacements } from '../../types'; +import { addSubFeatureReplacements } from '../../tools'; +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 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', + [SecuritySubFeatureId.trustedApplications]: [ { - 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'], - }, - ], + feature: SECURITY_FEATURE_ID_V3, + additionalPrivileges: { trusted_applications_all: ['global_artifact_management_all'] }, }, ], -}); - -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', + [SecuritySubFeatureId.hostIsolationExceptionsBasic]: [ { - 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'], - }, - ], + feature: SECURITY_FEATURE_ID_V3, + additionalPrivileges: { host_isolation_exceptions_all: ['global_artifact_management_all'] }, }, ], -}); -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: [ + [SecuritySubFeatureId.blocklist]: [ { - 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'], - }, - ], + feature: SECURITY_FEATURE_ID_V3, + additionalPrivileges: { blocklist_all: ['global_artifact_management_all'] }, }, ], -}); - -// 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', + [SecuritySubFeatureId.eventFilters]: [ { - 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'], - }, - ], + feature: SECURITY_FEATURE_ID_V3, + additionalPrivileges: { event_filters_all: ['global_artifact_management_all'] }, }, ], -}); - -// 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,45 @@ 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 subFeatures = securitySubFeaturesList.map<[SecuritySubFeatureId, SubFeatureConfig]>( + ([id, rawSubFeature]) => { + let subFeature = rawSubFeature; + + const featureReplacements = replacements[id]; + if (featureReplacements) { + subFeature = addSubFeatureReplacements(subFeature, featureReplacements); + } - const securitySubFeaturesMap = new Map( - securitySubFeaturesList + // If the feature is enabled for space awareness, we need to set false to the requireAllSpaces flag and remove the privilegesTooltip + // to avoid showing the "Requires all spaces" tooltip in the UI. + if (experimentalFeatures.endpointManagementSpaceAwarenessEnabled) { + subFeature = { ...subFeature, requireAllSpaces: true, privilegesTooltip: undefined }; + } + + return [id, subFeature]; + } ); + const securitySubFeaturesMap = new Map(subFeatures); + return Object.freeze(securitySubFeaturesMap); }; diff --git a/x-pack/solutions/security/packages/features/src/security/v1_features/kibana_sub_features_old.ts b/x-pack/solutions/security/packages/features/src/security/v1_features/kibana_sub_features_old.ts new file mode 100644 index 0000000000000..970078c392830 --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/security/v1_features/kibana_sub_features_old.ts @@ -0,0 +1,811 @@ +/* + * 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 { 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'], + }, + ], + }, + ], +}); + +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: [ + { + 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'], + }, + ], + }, + ], +}); +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: [ + { + 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'], + }, + ], + }, + ], +}); +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: [ + { + 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'], + }, + ], + }, + ], +}); +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: [ + { + 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'], + }, + ], + }, + ], +}); +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`], + }, + ], + }, + ], +}); + +/** + * Sub-features that will always be available for Security + * regardless of the product type. + */ +export const getSecurityBaseKibanaSubFeatureIds = ( + { experimentalFeatures }: SecurityFeatureParams // currently un-used, but left here as a convenience for possible future use +): SecuritySubFeatureId[] => []; + +/** + * 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())], + ]; + + // Use the following code to add feature based on feature flag + // if (experimentalFeatures.featureFlagName) { + // securitySubFeaturesList.push([SecuritySubFeatureId.featureId, featureSubFeature]); + // } + + const securitySubFeaturesMap = new Map( + securitySubFeaturesList + ); + + return Object.freeze(securitySubFeaturesMap); +}; 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..dd8119a54e71f 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 '../../tools'; + +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', + [SecuritySubFeatureId.globalArtifactManagement]: [{ feature: SECURITY_FEATURE_ID_V3 }], + [SecuritySubFeatureId.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: [ - { - 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', + [SecuritySubFeatureId.hostIsolationExceptionsBasic]: [ { - 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: 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', + [SecuritySubFeatureId.blocklist]: [ { - defaultMessage: - 'Extend Elastic Defend’s protection against malicious processes and protect against potentially harmful applications.', - } - ), - privilegeGroups: [ - { - 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', + [SecuritySubFeatureId.eventFilters]: [ { - 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: [ - { - 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'], - }, - ], - }, - ], -}); -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'], - }, - ], + feature: SECURITY_FEATURE_ID_V3, + additionalPrivileges: { event_filters_all: ['global_artifact_management_all'] }, }, ], -}); -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,62 +85,38 @@ 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; - } + const securitySubFeaturesList: Array<[SecuritySubFeatureId, SubFeatureConfig]> = [ + [SecuritySubFeatureId.endpointList, endpointListSubFeature()], - return subFeature; - }; + // ...((experimentalFeatures.defendInsights + // ? [[SecuritySubFeatureId.workflowInsights, workflowInsightsSubFeature()]] + // : []) as Array<[SecuritySubFeatureId, SubFeatureConfig]>), - const securitySubFeaturesList: Array<[SecuritySubFeatureId, SubFeatureConfig]> = [ - [SecuritySubFeatureId.endpointList, enableSpaceAwarenessIfNeeded(endpointListSubFeature())], - [ - SecuritySubFeatureId.endpointExceptions, - enableSpaceAwarenessIfNeeded(endpointExceptionsSubFeature()), - ], + [SecuritySubFeatureId.endpointExceptions, endpointExceptionsSubFeature()], ...((experimentalFeatures.endpointManagementSpaceAwarenessEnabled ? [ [ SecuritySubFeatureId.globalArtifactManagement, - enableSpaceAwarenessIfNeeded(globalArtifactManagementSubFeature()), + globalArtifactManagementSubFeature(experimentalFeatures), ], ] : []) as Array<[SecuritySubFeatureId, SubFeatureConfig]>), - [ - 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.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 @@ -927,13 +128,29 @@ export const getSecurityV2SubFeaturesMap = ({ // place with other All/Read/None options securitySubFeaturesList.splice(1, 0, [ SecuritySubFeatureId.workflowInsights, - enableSpaceAwarenessIfNeeded(workflowInsightsSubFeature()), + workflowInsightsSubFeature(), ]); } - const securitySubFeaturesMap = new Map( - securitySubFeaturesList + const subFeatures = securitySubFeaturesList.map<[SecuritySubFeatureId, SubFeatureConfig]>( + ([id, rawSubFeature]) => { + let subFeature = rawSubFeature; + + const featureReplacements = replacements[id]; + if (featureReplacements) { + subFeature = addSubFeatureReplacements(subFeature, featureReplacements); + } + + // If the feature is enabled for space awareness, we need to set false to the requireAllSpaces flag and remove the privilegesTooltip + // to avoid showing the "Requires all spaces" tooltip in the UI. + if (experimentalFeatures.endpointManagementSpaceAwarenessEnabled) { + subFeature = { ...subFeature, requireAllSpaces: true, privilegesTooltip: undefined }; + } + + return [id, subFeature]; + } ); + const securitySubFeaturesMap = new Map(subFeatures); return Object.freeze(securitySubFeaturesMap); }; diff --git a/x-pack/solutions/security/packages/features/src/security/v2_features/kibana_sub_features_old.ts b/x-pack/solutions/security/packages/features/src/security/v2_features/kibana_sub_features_old.ts new file mode 100644 index 0000000000000..de61b640a8e4b --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/security/v2_features/kibana_sub_features_old.ts @@ -0,0 +1,939 @@ +/* + * 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 { SecuritySubFeatureId } from '../../product_features_keys'; +import { APP_ID, 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'], + }, + ], + }, + ], +}); + +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: [ + { + 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'], + }, + ], + }, + ], +}); +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: [ + { + 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'], + }, + ], + }, + ], +}); +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: [ + { + 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'], + }, + ], + }, + ], +}); +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: [ + { + 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'], + }, + ], + }, + ], +}); +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'], + }, + ], + }, + ], +}); + +/** + * Sub-features that will always be available for Security + * regardless of the product type. + */ +export const getSecurityV2BaseKibanaSubFeatureIds = ( + { experimentalFeatures }: SecurityFeatureParams // currently un-used, but left here as a convenience for possible future use +): SecuritySubFeatureId[] => []; + +/** + * 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.endpointExceptions, + enableSpaceAwarenessIfNeeded(endpointExceptionsSubFeature()), + ], + + ...((experimentalFeatures.endpointManagementSpaceAwarenessEnabled + ? [ + [ + SecuritySubFeatureId.globalArtifactManagement, + enableSpaceAwarenessIfNeeded(globalArtifactManagementSubFeature()), + ], + ] + : []) as Array<[SecuritySubFeatureId, SubFeatureConfig]>), + + [ + 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())], + ]; + + // Use the following code to add feature based on feature flag + // if (experimentalFeatures.featureFlagName) { + // securitySubFeaturesList.push([SecuritySubFeatureId.featureId, featureSubFeature]); + // } + + if (experimentalFeatures.defendInsights) { + // place with other All/Read/None options + securitySubFeaturesList.splice(1, 0, [ + SecuritySubFeatureId.workflowInsights, + enableSpaceAwarenessIfNeeded(workflowInsightsSubFeature()), + ]); + } + + const securitySubFeaturesMap = new Map( + securitySubFeaturesList + ); + + return Object.freeze(securitySubFeaturesMap); +}; 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 df024f5dc98c3..3a67419e76b41 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,773 +5,26 @@ * 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 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, +} from '../kibana_sub_features'; /** * Sub-features that will always be available for Security @@ -785,58 +38,27 @@ 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.endpointExceptions, endpointExceptionsSubFeature()], [ SecuritySubFeatureId.globalArtifactManagement, - enableSpaceAwarenessIfNeeded(globalArtifactManagementSubFeature(experimentalFeatures)), + globalArtifactManagementSubFeature(experimentalFeatures), ], - - [ - 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.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 @@ -848,13 +70,24 @@ export const getSecurityV3SubFeaturesMap = ({ // place with other All/Read/None options securitySubFeaturesList.splice(1, 0, [ SecuritySubFeatureId.workflowInsights, - enableSpaceAwarenessIfNeeded(workflowInsightsSubFeature()), + workflowInsightsSubFeature(), ]); } - const securitySubFeaturesMap = new Map( - securitySubFeaturesList + const subFeatures = securitySubFeaturesList.map<[SecuritySubFeatureId, SubFeatureConfig]>( + ([id, rawSubFeature]) => { + let subFeature = rawSubFeature; + + // If the feature is enabled for space awareness, we need to set false to the requireAllSpaces flag and remove the privilegesTooltip + // to avoid showing the "Requires all spaces" tooltip in the UI. + if (experimentalFeatures.endpointManagementSpaceAwarenessEnabled) { + subFeature = { ...subFeature, requireAllSpaces: true, privilegesTooltip: undefined }; + } + + return [id, subFeature]; + } ); + const securitySubFeaturesMap = new Map(subFeatures); 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 f2cc84996d252..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 @@ -11,7 +11,5 @@ 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/timeline/index.ts b/x-pack/solutions/security/packages/features/src/timeline/index.ts index 39693820988e7..b508f9da9dff8 100644 --- a/x-pack/solutions/security/packages/features/src/timeline/index.ts +++ b/x-pack/solutions/security/packages/features/src/timeline/index.ts @@ -12,7 +12,5 @@ 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/tools.ts b/x-pack/solutions/security/packages/features/src/tools.ts index e6418a7a9b96d..2118a0a72810c 100644 --- a/x-pack/solutions/security/packages/features/src/tools.ts +++ b/x-pack/solutions/security/packages/features/src/tools.ts @@ -5,8 +5,15 @@ * 2.0. */ -import { mergeWith } from 'lodash'; -import type { ProductFeatureKeyType, ProductFeaturesConfig } from './types'; +import { cloneDeep, mergeWith } from 'lodash'; +import type { SubFeatureConfig } from '@kbn/features-plugin/common'; +import type { + AppSubFeaturesMap, + MutableSubFeatureConfig, + ProductFeatureKeyType, + ProductFeaturesConfig, + SubFeatureReplacement, +} from './types'; /** * Extends multiple ProductFeaturesConfig objects into a single one. @@ -29,3 +36,56 @@ export const extendProductFeatureConfigs = < return undefined; // Use default merge behavior for other 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.omitPrivilegeCopy ? [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/src/types.ts b/x-pack/solutions/security/packages/features/src/types.ts index e63a76a92e3e8..b9bd68f3d5ed9 100644 --- a/x-pack/solutions/security/packages/features/src/types.ts +++ b/x-pack/solutions/security/packages/features/src/types.ts @@ -102,9 +102,9 @@ export interface ProductFeatureParams< S extends string = string > { baseKibanaFeature: BaseKibanaFeatureConfig; - baseKibanaSubFeatureIds: S[]; - subFeaturesMap: AppSubFeaturesMap; - productFeatureConfig: ProductFeaturesConfig; + baseKibanaSubFeatureIds?: S[]; + subFeaturesMap?: AppSubFeaturesMap; + productFeatureConfig?: ProductFeaturesConfig; } export interface ConfigExtensions { @@ -132,3 +132,13 @@ export interface ProductFeaturesConfigurator { } 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 */ + omitPrivilegeCopy?: boolean; +} +export type SubFeatureReplacements = SubFeatureReplacement[]; 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 0e4c3cbbd7e01..ce06b62b79e24 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 @@ -57,7 +57,7 @@ export class ProductFeatures { const versionExtensions = versionsExtensions[featureVersion.baseKibanaFeature.id] ?? {}; const extendedConfig = extendProductFeatureConfigs( - featureVersion.productFeatureConfig, + featureVersion.productFeatureConfig ?? {}, allVersionsExtensions, versionExtensions ); @@ -75,12 +75,12 @@ export class ProductFeatures { const featureConfigMerger = new ProductFeaturesConfigMerger( this.logger, - featureVersion.subFeaturesMap + featureVersion.subFeaturesMap ?? new Map() ); const completeProductFeatureConfig = featureConfigMerger.mergeProductFeatureConfigs( featureVersion.baseKibanaFeature, - featureVersion.baseKibanaSubFeatureIds, + featureVersion.baseKibanaSubFeatureIds ?? [], filteredConfig ); 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..0ed27b47e3eb5 --- /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]: { + featureConfigModifier: updateGlobalArtifactManageReplacements, + }, + }, + siemV2: { + [ProductFeatureSecurityKey.endpointArtifactManagement]: { + featureConfigModifier: 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_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..2e2c33ccfe1e0 --- /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]: { + featureConfigModifier: updateGlobalArtifactManageReplacements, + }, + }, + siemV2: { + [ProductFeatureSecurityKey.endpointArtifactManagement]: { + featureConfigModifier: 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', + ]; + } +} From 1d811207c1576b9e582b6fbaeb16f2cacc67fb06 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Thu, 31 Jul 2025 14:15:03 +0200 Subject: [PATCH 17/28] clean old files --- .../v1_features/kibana_sub_features_old.ts | 811 --------------- .../v2_features/kibana_sub_features_old.ts | 939 ------------------ 2 files changed, 1750 deletions(-) delete mode 100644 x-pack/solutions/security/packages/features/src/security/v1_features/kibana_sub_features_old.ts delete mode 100644 x-pack/solutions/security/packages/features/src/security/v2_features/kibana_sub_features_old.ts diff --git a/x-pack/solutions/security/packages/features/src/security/v1_features/kibana_sub_features_old.ts b/x-pack/solutions/security/packages/features/src/security/v1_features/kibana_sub_features_old.ts deleted file mode 100644 index 970078c392830..0000000000000 --- a/x-pack/solutions/security/packages/features/src/security/v1_features/kibana_sub_features_old.ts +++ /dev/null @@ -1,811 +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 { 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 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'], - }, - ], - }, - ], -}); - -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: [ - { - 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'], - }, - ], - }, - ], -}); -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: [ - { - 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'], - }, - ], - }, - ], -}); -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: [ - { - 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'], - }, - ], - }, - ], -}); -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: [ - { - 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'], - }, - ], - }, - ], -}); -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`], - }, - ], - }, - ], -}); - -/** - * Sub-features that will always be available for Security - * regardless of the product type. - */ -export const getSecurityBaseKibanaSubFeatureIds = ( - { experimentalFeatures }: SecurityFeatureParams // currently un-used, but left here as a convenience for possible future use -): SecuritySubFeatureId[] => []; - -/** - * 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())], - ]; - - // Use the following code to add feature based on feature flag - // if (experimentalFeatures.featureFlagName) { - // securitySubFeaturesList.push([SecuritySubFeatureId.featureId, featureSubFeature]); - // } - - const securitySubFeaturesMap = new Map( - securitySubFeaturesList - ); - - return Object.freeze(securitySubFeaturesMap); -}; diff --git a/x-pack/solutions/security/packages/features/src/security/v2_features/kibana_sub_features_old.ts b/x-pack/solutions/security/packages/features/src/security/v2_features/kibana_sub_features_old.ts deleted file mode 100644 index de61b640a8e4b..0000000000000 --- a/x-pack/solutions/security/packages/features/src/security/v2_features/kibana_sub_features_old.ts +++ /dev/null @@ -1,939 +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 { 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 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'], - }, - ], - }, - ], -}); - -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: [ - { - 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'], - }, - ], - }, - ], -}); -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: [ - { - 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'], - }, - ], - }, - ], -}); -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: [ - { - 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'], - }, - ], - }, - ], -}); -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: [ - { - 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'], - }, - ], - }, - ], -}); -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'], - }, - ], - }, - ], -}); - -/** - * Sub-features that will always be available for Security - * regardless of the product type. - */ -export const getSecurityV2BaseKibanaSubFeatureIds = ( - { experimentalFeatures }: SecurityFeatureParams // currently un-used, but left here as a convenience for possible future use -): SecuritySubFeatureId[] => []; - -/** - * 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.endpointExceptions, - enableSpaceAwarenessIfNeeded(endpointExceptionsSubFeature()), - ], - - ...((experimentalFeatures.endpointManagementSpaceAwarenessEnabled - ? [ - [ - SecuritySubFeatureId.globalArtifactManagement, - enableSpaceAwarenessIfNeeded(globalArtifactManagementSubFeature()), - ], - ] - : []) as Array<[SecuritySubFeatureId, SubFeatureConfig]>), - - [ - 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())], - ]; - - // Use the following code to add feature based on feature flag - // if (experimentalFeatures.featureFlagName) { - // securitySubFeaturesList.push([SecuritySubFeatureId.featureId, featureSubFeature]); - // } - - if (experimentalFeatures.defendInsights) { - // place with other All/Read/None options - securitySubFeaturesList.splice(1, 0, [ - SecuritySubFeatureId.workflowInsights, - enableSpaceAwarenessIfNeeded(workflowInsightsSubFeature()), - ]); - } - - const securitySubFeaturesMap = new Map( - securitySubFeaturesList - ); - - return Object.freeze(securitySubFeaturesMap); -}; From bb421aec22b533563962c5a97f9f5d278ada016f Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Thu, 31 Jul 2025 19:32:49 +0200 Subject: [PATCH 18/28] tests fixed --- .../cases/v1_features/kibana_sub_features.ts | 2 +- .../cases/v2_features/kibana_sub_features.ts | 2 +- .../packages/features/src/helpers.test.ts | 2 +- .../v1_features/kibana_sub_features.ts | 16 +- .../v1_features/product_feature_config.ts | 2 +- .../v2_features/kibana_sub_features.ts | 55 ++-- .../v2_features/product_feature_config.ts | 2 +- .../v3_features/kibana_sub_features.ts | 31 +- .../security/packages/features/src/tools.ts | 91 ------ .../security/packages/features/src/types.ts | 2 +- .../security/packages/features/tools.ts | 7 - .../product_features.test.ts | 203 +++++++++---- .../product_features.ts | 2 +- .../product_features_config_merger.ts | 16 +- .../product_features_service.test.ts | 274 ++++++++++++------ 15 files changed, 376 insertions(+), 331 deletions(-) delete mode 100644 x-pack/solutions/security/packages/features/src/tools.ts delete mode 100644 x-pack/solutions/security/packages/features/tools.ts 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 8f79541a8b17d..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 @@ -9,7 +9,7 @@ import type { SubFeatureConfig } from '@kbn/features-plugin/common'; import { CasesSubFeatureId } from '../../product_features_keys'; import { CASES_FEATURE_ID_V3 } from '../../constants'; import type { CasesFeatureParams } from '../types'; -import { addAllSubFeatureReplacements } from '../../tools'; +import { addAllSubFeatureReplacements } from '../../utils'; import { getDeleteCasesSubFeature, getCasesSettingsCasesSubFeature } from '../kibana_sub_features'; /** 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 2733150c332c5..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 @@ -15,7 +15,7 @@ import { getCasesAddCommentsCasesSubFeature, getCasesReopenCaseSubFeature, } from '../kibana_sub_features'; -import { addAllSubFeatureReplacements } from '../../tools'; +import { addAllSubFeatureReplacements } from '../../utils'; /** * Sub-features that will always be available for Security Cases diff --git a/x-pack/solutions/security/packages/features/src/helpers.test.ts b/x-pack/solutions/security/packages/features/src/helpers.test.ts index 852eb38129795..763add808992b 100644 --- a/x-pack/solutions/security/packages/features/src/helpers.test.ts +++ b/x-pack/solutions/security/packages/features/src/helpers.test.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { createEnabledProductFeaturesConfigMap } from './tools'; +import { createEnabledProductFeaturesConfigMap } from './utils'; import { ProductFeatureSecurityKey } from './product_features_keys'; import type { ProductFeatureKibanaConfig, ProductFeatureKeys } from './types'; 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 3ce3462b4bdbb..b4056afa4a097 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 @@ -10,7 +10,7 @@ import { SECURITY_FEATURE_ID_V3 } from '../../../constants'; import { SecuritySubFeatureId } from '../../product_features_keys'; import type { SecurityFeatureParams } from '../types'; import type { SubFeatureReplacements } from '../../types'; -import { addSubFeatureReplacements } from '../../tools'; +import { addSubFeatureReplacements } from '../../utils'; import { endpointListSubFeature, endpointExceptionsSubFeature, @@ -99,26 +99,22 @@ export const getSecuritySubFeaturesMap = ({ [SecuritySubFeatureId.scanAction, scanActionSubFeature()], ]; - const subFeatures = securitySubFeaturesList.map<[SecuritySubFeatureId, SubFeatureConfig]>( - ([id, rawSubFeature]) => { - let subFeature = rawSubFeature; + const securitySubFeaturesMap = new Map( + securitySubFeaturesList.map(([id, originalSubFeature]) => { + let subFeature = originalSubFeature; const featureReplacements = replacements[id]; if (featureReplacements) { subFeature = addSubFeatureReplacements(subFeature, featureReplacements); } - // If the feature is enabled for space awareness, we need to set false to the requireAllSpaces flag and remove the privilegesTooltip - // to avoid showing the "Requires all spaces" tooltip in the UI. + // 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: true, privilegesTooltip: undefined }; } return [id, subFeature]; - } + }) ); - - const securitySubFeaturesMap = new Map(subFeatures); - 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 index 17f356b47277b..cb3fbb8bae446 100644 --- 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 @@ -8,7 +8,7 @@ import { ProductFeatureSecurityKey } from '../../product_features_keys'; import { APP_ID } from '../../constants'; import type { SecurityProductFeaturesConfig } from '../types'; -import { extendProductFeatureConfigs } from '../../tools'; +import { extendProductFeatureConfigs } from '../../utils'; import { securityDefaultProductFeaturesConfig } from '../product_feature_config'; export const securityV1ProductFeaturesConfig: SecurityProductFeaturesConfig = 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 dd8119a54e71f..687fbe47434cc 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 @@ -28,7 +28,7 @@ import { workflowInsightsSubFeature, } from '../kibana_sub_features'; import type { SubFeatureReplacements } from '../../types'; -import { addSubFeatureReplacements } from '../../tools'; +import { addSubFeatureReplacements } from '../../utils'; const replacements: Partial> = { [SecuritySubFeatureId.endpointList]: [{ feature: SECURITY_FEATURE_ID_V3 }], @@ -90,22 +90,12 @@ export const getSecurityV2SubFeaturesMap = ({ }: SecurityFeatureParams): Map => { const securitySubFeaturesList: Array<[SecuritySubFeatureId, SubFeatureConfig]> = [ [SecuritySubFeatureId.endpointList, endpointListSubFeature()], - - // ...((experimentalFeatures.defendInsights - // ? [[SecuritySubFeatureId.workflowInsights, workflowInsightsSubFeature()]] - // : []) as Array<[SecuritySubFeatureId, SubFeatureConfig]>), - + [SecuritySubFeatureId.workflowInsights, workflowInsightsSubFeature()], [SecuritySubFeatureId.endpointExceptions, endpointExceptionsSubFeature()], - - ...((experimentalFeatures.endpointManagementSpaceAwarenessEnabled - ? [ - [ - SecuritySubFeatureId.globalArtifactManagement, - globalArtifactManagementSubFeature(experimentalFeatures), - ], - ] - : []) as Array<[SecuritySubFeatureId, SubFeatureConfig]>), - + [ + SecuritySubFeatureId.globalArtifactManagement, + globalArtifactManagementSubFeature(experimentalFeatures), + ], [SecuritySubFeatureId.trustedApplications, trustedApplicationsSubFeature()], [SecuritySubFeatureId.hostIsolationExceptionsBasic, hostIsolationExceptionsBasicSubFeature()], [SecuritySubFeatureId.blocklist, blocklistSubFeature()], @@ -119,38 +109,31 @@ export const getSecurityV2SubFeaturesMap = ({ [SecuritySubFeatureId.scanAction, scanActionSubFeature()], ]; - // Use the following code to add feature based on feature flag - // if (experimentalFeatures.featureFlagName) { - // securitySubFeaturesList.push([SecuritySubFeatureId.featureId, featureSubFeature]); - // } - - if (experimentalFeatures.defendInsights) { - // place with other All/Read/None options - securitySubFeaturesList.splice(1, 0, [ - SecuritySubFeatureId.workflowInsights, - workflowInsightsSubFeature(), - ]); - } - - const subFeatures = securitySubFeaturesList.map<[SecuritySubFeatureId, SubFeatureConfig]>( - ([id, rawSubFeature]) => { - let subFeature = rawSubFeature; + const securitySubFeaturesMap = new Map( + securitySubFeaturesList.map(([id, originalSubFeature]) => { + let subFeature = originalSubFeature; const featureReplacements = replacements[id]; if (featureReplacements) { subFeature = addSubFeatureReplacements(subFeature, featureReplacements); } - // If the feature is enabled for space awareness, we need to set false to the requireAllSpaces flag and remove the privilegesTooltip - // to avoid showing the "Requires all spaces" tooltip in the UI. + // 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: true, privilegesTooltip: undefined }; } return [id, subFeature]; - } + }) ); - const securitySubFeaturesMap = new Map(subFeatures); + // Remove disabled experimental features + if (!experimentalFeatures.endpointManagementSpaceAwarenessEnabled) { + securitySubFeaturesMap.delete(SecuritySubFeatureId.globalArtifactManagement); + } + if (!experimentalFeatures.defendInsights) { + securitySubFeaturesMap.delete(SecuritySubFeatureId.workflowInsights); + } + 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 index 4f7ff6c6153d3..fc4eb504bc72a 100644 --- 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 @@ -8,7 +8,7 @@ import { ProductFeatureSecurityKey } from '../../product_features_keys'; import { APP_ID } from '../../constants'; import type { SecurityProductFeaturesConfig } from '../types'; -import { extendProductFeatureConfigs } from '../../tools'; +import { extendProductFeatureConfigs } from '../../utils'; import { securityDefaultProductFeaturesConfig } from '../product_feature_config'; export const securityV2ProductFeaturesConfig: SecurityProductFeaturesConfig = 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 3a67419e76b41..03b63dac6b934 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 @@ -43,6 +43,7 @@ export const getSecurityV3SubFeaturesMap = ({ }: SecurityFeatureParams): Map => { const securitySubFeaturesList: Array<[SecuritySubFeatureId, SubFeatureConfig]> = [ [SecuritySubFeatureId.endpointList, endpointListSubFeature()], + [SecuritySubFeatureId.workflowInsights, workflowInsightsSubFeature()], [SecuritySubFeatureId.endpointExceptions, endpointExceptionsSubFeature()], [ SecuritySubFeatureId.globalArtifactManagement, @@ -61,33 +62,23 @@ export const getSecurityV3SubFeaturesMap = ({ [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, - workflowInsightsSubFeature(), - ]); - } - - const subFeatures = securitySubFeaturesList.map<[SecuritySubFeatureId, SubFeatureConfig]>( - ([id, rawSubFeature]) => { - let subFeature = rawSubFeature; - - // If the feature is enabled for space awareness, we need to set false to the requireAllSpaces flag and remove the privilegesTooltip - // to avoid showing the "Requires all spaces" tooltip in the UI. + // 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: true, privilegesTooltip: undefined }; } return [id, subFeature]; - } + }) ); - const securitySubFeaturesMap = new Map(subFeatures); + // Remove disabled experimental features + if (!experimentalFeatures.defendInsights) { + securitySubFeaturesMap.delete(SecuritySubFeatureId.workflowInsights); + } + return Object.freeze(securitySubFeaturesMap); }; diff --git a/x-pack/solutions/security/packages/features/src/tools.ts b/x-pack/solutions/security/packages/features/src/tools.ts deleted file mode 100644 index 2118a0a72810c..0000000000000 --- a/x-pack/solutions/security/packages/features/src/tools.ts +++ /dev/null @@ -1,91 +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 { cloneDeep, mergeWith } from 'lodash'; -import type { SubFeatureConfig } from '@kbn/features-plugin/common'; -import type { - AppSubFeaturesMap, - MutableSubFeatureConfig, - ProductFeatureKeyType, - ProductFeaturesConfig, - SubFeatureReplacement, -} from './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, (objValue: unknown, srcValue: unknown) => { - if (Array.isArray(objValue) && Array.isArray(srcValue)) { - return [...new Set([...objValue, ...srcValue])]; - } - return undefined; // Use default merge behavior for other 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.omitPrivilegeCopy ? [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/src/types.ts b/x-pack/solutions/security/packages/features/src/types.ts index b9bd68f3d5ed9..65b71db11dfcf 100644 --- a/x-pack/solutions/security/packages/features/src/types.ts +++ b/x-pack/solutions/security/packages/features/src/types.ts @@ -139,6 +139,6 @@ export interface SubFeatureReplacement { /** 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 */ - omitPrivilegeCopy?: boolean; + skipPrivilegeCopy?: boolean; } export type SubFeatureReplacements = SubFeatureReplacement[]; diff --git a/x-pack/solutions/security/packages/features/tools.ts b/x-pack/solutions/security/packages/features/tools.ts deleted file mode 100644 index ebc0523e89f05..0000000000000 --- a/x-pack/solutions/security/packages/features/tools.ts +++ /dev/null @@ -1,7 +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 * from './src/tools'; 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 18890e65cfbfe..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 @@ -54,7 +54,6 @@ const baseKibanaFeature: BaseKibanaFeatureConfig = { }; // sub-features definition - const SUB_FEATURE: SubFeatureConfig = { name: 'subFeature1', privilegeGroups: [ @@ -76,6 +75,7 @@ const SUB_FEATURE: SubFeatureConfig = { }, ], }; + const SUB_FEATURE_2: SubFeatureConfig = { name: 'subFeature2', privilegeGroups: [ @@ -146,11 +146,15 @@ 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; @@ -160,13 +164,21 @@ const featuresSetup = { } as unknown as FeaturesPluginSetup; describe('ProductFeatures', () => { - describe('setConfig', () => { - it('should register base kibana features', () => { + 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.register(featureGroup, new Map()); + productFeatures.register([], {}); expect(featuresSetup.registerKibanaFeature).toHaveBeenCalledWith({ ...baseKibanaFeature, @@ -177,12 +189,19 @@ describe('ProductFeatures', () => { it('should register enabled kibana features', () => { const productFeatures = new ProductFeatures(logger); - productFeatures.create(featureGroup, [testFeatureParams]); + // Setup product feature with static config + const paramsWithConfig = { + ...testFeatureParams, + productFeatureConfig: { + [testFeatureKey1]: testFeaturePrivilegeConfig, + }, + }; + + productFeatures.create(featureGroup, [paramsWithConfig]); productFeatures.init(featuresSetup); - productFeatures.register( - featureGroup, - new Map([['test-feature' as ProductFeatureKeyType, testFeaturePrivilegeConfig]]) - ); + + // Register with the specific enabled key + productFeatures.register([testFeatureKey1], {}); expect(featuresSetup.registerKibanaFeature).toHaveBeenCalledWith({ ...baseKibanaFeature, @@ -194,15 +213,20 @@ describe('ProductFeatures', () => { it('should register enabled kibana features and sub features', () => { const productFeatures = new ProductFeatures(logger); - productFeatures.create(featureGroup, [testFeatureParams]); + // 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.register( - featureGroup, - 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, @@ -214,17 +238,21 @@ describe('ProductFeatures', () => { it('should register enabled kibana features and default sub features', () => { const productFeatures = new ProductFeatures(logger); - productFeatures.create(featureGroup, [ - { ...testFeatureParams, baseKibanaSubFeatureIds: [SUB_FEATURE_2.name] }, - ]); + // 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.register( - featureGroup, - 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, @@ -232,6 +260,70 @@ 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', () => { @@ -239,7 +331,7 @@ describe('ProductFeatures', () => { const productFeatures = new ProductFeatures(logger); productFeatures.create(featureGroup, [testFeatureParams]); productFeatures.init(featuresSetup); - productFeatures.register(featureGroup, new Map()); + productFeatures.register([], {}); expect(productFeatures.isActionRegistered('api:api-read')).toEqual(true); expect(productFeatures.isActionRegistered('ui:read')).toEqual(true); @@ -255,12 +347,17 @@ describe('ProductFeatures', () => { it('should register config privilege actions', () => { const productFeatures = new ProductFeatures(logger); - productFeatures.create(featureGroup, [testFeatureParams]); + + const paramsWithConfig = { + ...testFeatureParams, + productFeatureConfig: { + [testFeatureKey1]: testFeaturePrivilegeConfig, + }, + }; + + productFeatures.create(featureGroup, [paramsWithConfig]); productFeatures.init(featuresSetup); - productFeatures.register( - featureGroup, - 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); @@ -277,15 +374,17 @@ describe('ProductFeatures', () => { it('should register config sub-feature privilege actions', () => { const productFeatures = new ProductFeatures(logger); - productFeatures.create(featureGroup, [testFeatureParams]); + const paramsWithConfig = { + ...testFeatureParams, + productFeatureConfig: { + [testFeatureKey1]: testFeaturePrivilegeConfig, + [testFeatureKey2]: testSubFeaturePrivilegeConfig, + }, + }; + + productFeatures.create(featureGroup, [paramsWithConfig]); productFeatures.init(featuresSetup); - productFeatures.register( - featureGroup, - 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); @@ -301,17 +400,19 @@ describe('ProductFeatures', () => { it('should register default and config sub-feature privilege actions', () => { const productFeatures = new ProductFeatures(logger); - productFeatures.create(featureGroup, [ - { ...testFeatureParams, baseKibanaSubFeatureIds: [SUB_FEATURE_2.name] }, - ]); + + const paramsWithConfig = { + ...testFeatureParams, + baseKibanaSubFeatureIds: [SUB_FEATURE_2.name], + productFeatureConfig: { + [testFeatureKey1]: testFeaturePrivilegeConfig, + [testFeatureKey2]: testSubFeaturePrivilegeConfig, + }, + }; + + productFeatures.create(featureGroup, [paramsWithConfig]); productFeatures.init(featuresSetup); - productFeatures.register( - featureGroup, - 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 ce06b62b79e24..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 @@ -18,7 +18,7 @@ import type { ProductFeaturesConfiguratorExtensions, ProductFeatureKibanaConfig, } from '@kbn/security-solution-features'; -import { extendProductFeatureConfigs } from '@kbn/security-solution-features/tools'; +import { extendProductFeatureConfigs } from '@kbn/security-solution-features/utils'; import { ProductFeaturesConfigMerger } from './product_features_config_merger'; export class ProductFeatures { 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 7bfbaa810846f..304c37dbd2f91 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,7 +5,7 @@ * 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 { @@ -16,6 +16,7 @@ import type { MutableKibanaFeatureConfig, MutableSubFeatureConfig, } from '@kbn/security-solution-features'; +import { featureConfigMerger } from '@kbn/security-solution-features/utils'; export class ProductFeaturesConfigMerger { constructor( @@ -118,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 499df389bb7c2..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 @@ -10,13 +10,6 @@ import { ProductFeatures } from './product_features'; import type { BaseKibanaFeatureConfig, ProductFeaturesConfigurator, - AssistantProductFeaturesConfigMap, - AttackDiscoveryProductFeaturesConfigMap, - CasesProductFeaturesConfigMap, - NotesProductFeaturesConfigMap, - SecurityProductFeaturesConfigMap, - SiemMigrationsProductFeaturesConfigMap, - TimelineProductFeaturesConfigMap, } from '@kbn/security-solution-features'; import { loggerMock } from '@kbn/logging-mocks'; import type { ExperimentalFeatures } from '../../../common'; @@ -87,100 +80,191 @@ describe('ProductFeaturesService', () => { expect(MockedProductFeatures.mock.instances[0].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 + ); - productFeaturesService.setup(coreSetup, pluginsSetup); + 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); - const mockSecurityConfig = new Map() as SecurityProductFeaturesConfigMap; - const mockCasesConfig = new Map() as CasesProductFeaturesConfigMap; - const mockAssistantConfig = new Map() as AssistantProductFeaturesConfigMap; - const mockAttackDiscoveryConfig = new Map() as AttackDiscoveryProductFeaturesConfigMap; - const mockSiemMigrationsConfig = new Map() as SiemMigrationsProductFeaturesConfigMap; - const mockTimelineConfig = new Map() as TimelineProductFeaturesConfigMap; - const mockNotesConfig = new Map() as NotesProductFeaturesConfigMap; - - 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(); - - const { register } = MockedProductFeatures.mock.instances[0]; - expect(register).toHaveBeenCalledWith('security', mockSecurityConfig); - expect(register).toHaveBeenCalledWith('cases', mockCasesConfig); - expect(register).toHaveBeenCalledWith('securityAssistant', mockAssistantConfig); - expect(register).toHaveBeenCalledWith('attackDiscovery', mockAttackDiscoveryConfig); - expect(register).toHaveBeenCalledWith('siemMigrations', mockSiemMigrationsConfig); - expect(register).toHaveBeenCalledWith('timeline', mockTimelineConfig); - expect(register).toHaveBeenCalledWith('notes', mockNotesConfig); + // 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); + productFeaturesService.setup(coreSetup, pluginsSetup); - const mockSecurityConfig = new Map([ - [ProductFeatureKey.advancedInsights, {}], - [ProductFeatureKey.endpointExceptions, {}], - ]) as SecurityProductFeaturesConfigMap; - const mockCasesConfig = new Map([ - [ProductFeatureKey.casesConnectors, {}], - ]) as CasesProductFeaturesConfigMap; - const mockAssistantConfig = new Map([ - [ProductFeatureKey.assistant, {}], - ]) as AssistantProductFeaturesConfigMap; - const mockAttackDiscoveryConfig = new Map([ - [ProductFeatureKey.attackDiscovery, {}], - ]) as AttackDiscoveryProductFeaturesConfigMap; - const mockSiemMigrationsConfig = new Map([ - [ProductFeatureKey.siemMigrations, {}], - ]) as SiemMigrationsProductFeaturesConfigMap; - const mockTimelineConfig = new Map([ - [ProductFeatureKey.timeline, {}], - ]) as TimelineProductFeaturesConfigMap; - const mockNotesConfig = new Map([ - [ProductFeatureKey.notes, {}], - ]) as NotesProductFeaturesConfigMap; - - 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); + // 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 + ); + + 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 + ); + }); + }); + + describe('getApiActionName', () => { + it('should return API action name with proper prefix', () => { + const experimentalFeatures = {} as ExperimentalFeatures; + const productFeaturesService = new ProductFeaturesService( + loggerMock.create(), + experimentalFeatures + ); + + expect(productFeaturesService.getApiActionName('test')).toEqual('api:securitySolution-test'); + expect(productFeaturesService.getApiActionName('case/create')).toEqual( + 'api:securitySolution-case/create' + ); + }); + }); + + 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', () => { From 2c406bc3b5063a32b383aa79a66889541173ec69 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Thu, 31 Jul 2025 19:33:22 +0200 Subject: [PATCH 19/28] feature utils --- .../packages/features/src/utils/index.ts | 9 + .../src/utils/product_feature_config.test.ts | 254 ++++++++++++++++ .../src/utils/product_feature_config.ts | 41 +++ .../features/src/utils/sub_features.test.ts | 278 ++++++++++++++++++ .../features/src/utils/sub_features.ts | 63 ++++ .../security/packages/features/utils.ts | 7 + 6 files changed, 652 insertions(+) create mode 100644 x-pack/solutions/security/packages/features/src/utils/index.ts create mode 100644 x-pack/solutions/security/packages/features/src/utils/product_feature_config.test.ts create mode 100644 x-pack/solutions/security/packages/features/src/utils/product_feature_config.ts create mode 100644 x-pack/solutions/security/packages/features/src/utils/sub_features.test.ts create mode 100644 x-pack/solutions/security/packages/features/src/utils/sub_features.ts create mode 100644 x-pack/solutions/security/packages/features/utils.ts 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..074ec8ffd2f43 --- /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', + skipPrivilegeCopy: 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 skipPrivilegeCopy is true', () => { + const replacements: SubFeatureReplacement[] = [ + { + feature: 'replacementFeature', + skipPrivilegeCopy: 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', + skipPrivilegeCopy: 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', + skipPrivilegeCopy: 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', + skipPrivilegeCopy: false, + }, + { + feature: 'feature2', + skipPrivilegeCopy: 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', + skipPrivilegeCopy: 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', + skipPrivilegeCopy: 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..4acb4ce84de7e --- /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.skipPrivilegeCopy ? [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'; From b2a2288fef87e21b4b9519fd571257150c76e8ff Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Thu, 31 Jul 2025 19:48:50 +0200 Subject: [PATCH 20/28] set require all spaces correctly --- .../features/src/security/v1_features/kibana_sub_features.ts | 2 +- .../features/src/security/v2_features/kibana_sub_features.ts | 2 +- .../features/src/security/v3_features/kibana_sub_features.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 b4056afa4a097..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 @@ -110,7 +110,7 @@ export const getSecuritySubFeaturesMap = ({ // 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: true, privilegesTooltip: undefined }; + subFeature = { ...subFeature, requireAllSpaces: false, privilegesTooltip: undefined }; } return [id, subFeature]; 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 687fbe47434cc..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 @@ -120,7 +120,7 @@ export const getSecurityV2SubFeaturesMap = ({ // 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: true, privilegesTooltip: undefined }; + subFeature = { ...subFeature, requireAllSpaces: false, privilegesTooltip: undefined }; } return [id, subFeature]; 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 03b63dac6b934..3496e5892a0ff 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 @@ -68,7 +68,7 @@ export const getSecurityV3SubFeaturesMap = ({ // 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: true, privilegesTooltip: undefined }; + subFeature = { ...subFeature, requireAllSpaces: false, privilegesTooltip: undefined }; } return [id, subFeature]; From a8e654a6d3cb63adc74dc83d215ad59d759ce25f Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Fri, 1 Aug 2025 09:56:20 +0200 Subject: [PATCH 21/28] extend subFeatures with gwriteGlobalArtifact --- .../features/src/security/kibana_sub_features.ts | 2 +- .../src/security/v1_features/product_feature_config.ts | 9 +++++++++ .../src/security/v2_features/product_feature_config.ts | 9 +++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) 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 index cf3ad8037bd44..71b5931a5e614 100644 --- 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 @@ -191,7 +191,7 @@ export const blocklistSubFeature = (): SubFeatureConfig => ({ 'securitySolutionPackages.features.featureRegistry.subFeatures.blockList.description', { defaultMessage: - "Extend Elastic Defend's protection against malicious processes and protect against potentially harmful applications.", + 'Extend Elastic Defend’s protection against malicious processes and protect against potentially harmful applications.', } ), privilegeGroups: [ 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 index cb3fbb8bae446..8fcb014c69c80 100644 --- 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 @@ -18,6 +18,15 @@ export const securityV1ProductFeaturesConfig: SecurityProductFeaturesConfig = // 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/product_feature_config.ts b/x-pack/solutions/security/packages/features/src/security/v2_features/product_feature_config.ts index fc4eb504bc72a..3ae4a96ec332a 100644 --- 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 @@ -18,6 +18,15 @@ export const securityV2ProductFeaturesConfig: SecurityProductFeaturesConfig = // 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`] }, + ], }, }); From b33a8cb3d543362d1b549ba6e7765b80c9b593f3 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 1 Aug 2025 08:09:37 +0000 Subject: [PATCH 22/28] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- .../security/plugins/security_solution_ess/tsconfig.json | 1 - .../security/plugins/security_solution_serverless/tsconfig.json | 1 - 2 files changed, 2 deletions(-) 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 722ca8fe4c50e..7201f7cc944ba 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/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", From 8a7ed5065081a4702bf6770eea70673759965aab Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Fri, 1 Aug 2025 10:57:24 +0200 Subject: [PATCH 23/28] cleaning test --- .../packages/features/src/helpers.test.ts | 72 ------------------- 1 file changed, 72 deletions(-) delete mode 100644 x-pack/solutions/security/packages/features/src/helpers.test.ts diff --git a/x-pack/solutions/security/packages/features/src/helpers.test.ts b/x-pack/solutions/security/packages/features/src/helpers.test.ts deleted file mode 100644 index 763add808992b..0000000000000 --- a/x-pack/solutions/security/packages/features/src/helpers.test.ts +++ /dev/null @@ -1,72 +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 { createEnabledProductFeaturesConfigMap } from './utils'; -import { ProductFeatureSecurityKey } from './product_features_keys'; -import type { ProductFeatureKibanaConfig, ProductFeatureKeys } from './types'; - -const productFeaturesConfigs: Partial< - Record -> = { - [ProductFeatureSecurityKey.advancedInsights]: { - privileges: { all: { ui: ['capability1'] } }, - }, - [ProductFeatureSecurityKey.automaticImport]: { - privileges: { all: { ui: ['capability2'] } }, - }, - [ProductFeatureSecurityKey.detections]: { - privileges: { all: { ui: ['capability3'] } }, - }, -}; - -describe('createEnabledProductFeaturesConfigMap', () => { - it('should return a Map with only enabled product features and their configs', () => { - const enabledProductFeaturesKeys = [ - ProductFeatureSecurityKey.advancedInsights, - ProductFeatureSecurityKey.automaticImport, - ] as unknown as ProductFeatureKeys; - - const result = createEnabledProductFeaturesConfigMap( - ProductFeatureSecurityKey, - productFeaturesConfigs, - enabledProductFeaturesKeys - ); - - expect(result.size).toBe(2); - expect(result.get(ProductFeatureSecurityKey.advancedInsights)).toEqual({ - privileges: { all: { ui: ['capability1'] } }, - }); - expect(result.get(ProductFeatureSecurityKey.automaticImport)).toEqual({ - privileges: { all: { ui: ['capability2'] } }, - }); - expect(result.has(ProductFeatureSecurityKey.detections)).toBe(false); - }); - - it('should return empty config object if config is missing for enabled key', () => { - const enabledKeys: ProductFeatureKeys = [ - ProductFeatureSecurityKey.detections, - ] as unknown as ProductFeatureKeys; - - const result = createEnabledProductFeaturesConfigMap( - ProductFeatureSecurityKey, - {}, // No specific configs provided for "detections" ProductFeatureKey - enabledKeys - ); - - expect(result.size).toBe(1); - expect(result.get(ProductFeatureSecurityKey.detections)).toEqual({}); - }); - - it('should return an empty Map if no features are enabled', () => { - const result = createEnabledProductFeaturesConfigMap( - ProductFeatureSecurityKey, - productFeaturesConfigs, - [] - ); - - expect(result.size).toBe(0); - }); -}); From b2b76e0b09a3c16d2851b123102f16b735954b39 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Fri, 1 Aug 2025 11:58:07 +0200 Subject: [PATCH 24/28] fix mock --- .../lib/product_features_service/mocks.ts | 130 +----------------- 1 file changed, 3 insertions(+), 127 deletions(-) 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 0cc42c610bf27..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 @@ -77,7 +77,7 @@ 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') @@ -88,133 +88,9 @@ export const createProductFeaturesServiceMock = ( 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, }); } From 146e807f09c71be24a0d772b27547df73a5aeffe Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Fri, 1 Aug 2025 15:07:15 +0200 Subject: [PATCH 25/28] update snapshot --- .../test_suites/security/platform_security/authorization.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/test_serverless/api_integration/test_suites/security/platform_security/authorization.ts b/x-pack/test_serverless/api_integration/test_suites/security/platform_security/authorization.ts index f2fb55c6621f4..cbc02802837fc 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/platform_security/authorization.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/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", From 62f988895742938156d513fb0c08b0fafaf0c967 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 21 Aug 2025 14:44:03 +0000 Subject: [PATCH 26/28] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- .../solutions/security/plugins/security_solution/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/tsconfig.json b/x-pack/solutions/security/plugins/security_solution/tsconfig.json index e9e6bdcaffece..dfe792e0a3a2a 100644 --- a/x-pack/solutions/security/plugins/security_solution/tsconfig.json +++ b/x-pack/solutions/security/plugins/security_solution/tsconfig.json @@ -259,4 +259,4 @@ "@kbn/licensing-types", "@kbn/core-metrics-server", ] -} \ No newline at end of file +} From f5e4f15251252b83b07dc5831f15a383d7623f95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20=C3=81brah=C3=A1m?= Date: Fri, 22 Aug 2025 10:14:24 +0200 Subject: [PATCH 27/28] change `featureConfigModifier` to an array --- .../security/packages/features/src/types.ts | 6 +- .../product_features_config_merger.test.ts | 68 +++++++++++++++++-- .../product_features_config_merger.ts | 10 +-- .../product_features_extensions.ts | 4 +- .../product_features_extensions.ts | 4 +- 5 files changed, 73 insertions(+), 19 deletions(-) diff --git a/x-pack/solutions/security/packages/features/src/types.ts b/x-pack/solutions/security/packages/features/src/types.ts index 65b71db11dfcf..deae1ce5c15fb 100644 --- a/x-pack/solutions/security/packages/features/src/types.ts +++ b/x-pack/solutions/security/packages/features/src/types.ts @@ -50,14 +50,14 @@ export type ProductFeatureKibanaConfig = subFeaturesPrivileges?: SubFeaturesPrivileges[]; /** - * Function to apply free modifications to the resulting Kibana feature config when a specific ProductFeatureKey is enabled. + * 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 kibanaFeatureConfig to be mutated * @returns void */ - featureConfigModifier?: FeatureConfigModifier; + featureConfigModifiers?: FeatureConfigModifier[]; }; /** @@ -69,7 +69,7 @@ export type ProductFeatureKibanaConfig = * - `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. - * - `featureConfigModifier`: a function to apply free modifications to the resulting Kibana feature config when a specific ProductFeatureKey is enabled. + * - `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, 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 17bd1afc3da95..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,18 +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'], - featureConfigModifier: jest.fn().mockImplementation((baseConfig: KibanaFeatureConfig) => { - baseConfig.name = 'NEW NAME'; - }), + featureConfigModifiers: [ + jest.fn().mockImplementation((baseConfig: KibanaFeatureConfig) => { + baseConfig.name = 'NEW NAME'; + }), + ], }, { - featureConfigModifier: jest.fn().mockImplementation((baseConfig: KibanaFeatureConfig) => { - baseConfig.order = 666; - }), + featureConfigModifiers: [ + jest.fn().mockImplementation((baseConfig: KibanaFeatureConfig) => { + baseConfig.order = 666; + }), + ], }, ]; @@ -382,6 +386,56 @@ describe('ProductFeaturesConfigMerger', () => { 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: '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 304c37dbd2f91..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 @@ -42,13 +42,13 @@ export class ProductFeaturesConfigMerger { kibanaSubFeatureIds.map((id) => [id, true]) ); const subFeaturesPrivilegesToMerge: SubFeaturesPrivileges[] = []; - const featureConfigModifiers: FeatureConfigModifier[] = []; + const allFeatureConfigModifiers: FeatureConfigModifier[] = []; productFeaturesConfigs.forEach((productFeatureConfig) => { const { subFeaturesPrivileges, subFeatureIds, - featureConfigModifier, + featureConfigModifiers, ...productFeatureConfigToMerge } = productFeatureConfig; @@ -60,8 +60,8 @@ export class ProductFeaturesConfigMerger { subFeaturesPrivilegesToMerge.push(...subFeaturesPrivileges); } - if (featureConfigModifier) { - featureConfigModifiers.push(featureConfigModifier); + if (featureConfigModifiers) { + allFeatureConfigModifiers.push(...featureConfigModifiers); } mergeWith(mergedKibanaFeatureConfig, productFeatureConfigToMerge, featureConfigMerger); @@ -83,7 +83,7 @@ export class ProductFeaturesConfigMerger { mergedKibanaFeatureConfig.subFeatures = mergedKibanaSubFeatures; // Apply custom modifications after merging all the product feature configs, including the subFeatures - featureConfigModifiers.forEach((modifier) => { + allFeatureConfigModifiers.forEach((modifier) => { modifier(mergedKibanaFeatureConfig); }); 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 index 0ed27b47e3eb5..8e177b0a5b6a9 100644 --- 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 @@ -31,12 +31,12 @@ export const productFeaturesExtensions: ProductFeaturesConfiguratorExtensions = version: { siem: { [ProductFeatureSecurityKey.endpointArtifactManagement]: { - featureConfigModifier: updateGlobalArtifactManageReplacements, + featureConfigModifiers: [updateGlobalArtifactManageReplacements], }, }, siemV2: { [ProductFeatureSecurityKey.endpointArtifactManagement]: { - featureConfigModifier: updateGlobalArtifactManageReplacements, + featureConfigModifiers: [updateGlobalArtifactManageReplacements], }, }, }, 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 index 2e2c33ccfe1e0..d856a37769061 100644 --- 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 @@ -24,12 +24,12 @@ export const productFeaturesExtensions: ProductFeaturesConfiguratorExtensions = version: { siem: { [ProductFeatureSecurityKey.endpointArtifactManagement]: { - featureConfigModifier: updateGlobalArtifactManageReplacements, + featureConfigModifiers: [updateGlobalArtifactManageReplacements], }, }, siemV2: { [ProductFeatureSecurityKey.endpointArtifactManagement]: { - featureConfigModifier: updateGlobalArtifactManageReplacements, + featureConfigModifiers: [updateGlobalArtifactManageReplacements], }, }, }, From 58f60311307968e6a0ddced8471eb226dc0cc362 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20=C3=81brah=C3=A1m?= Date: Fri, 22 Aug 2025 13:40:02 +0200 Subject: [PATCH 28/28] rename `skipPrivilegeCopy` to `removeOriginalPrivileges` for clarity in SubFeatureReplacement --- .../security/packages/features/src/types.ts | 7 +++++-- .../features/src/utils/sub_features.test.ts | 18 +++++++++--------- .../features/src/utils/sub_features.ts | 2 +- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/x-pack/solutions/security/packages/features/src/types.ts b/x-pack/solutions/security/packages/features/src/types.ts index deae1ce5c15fb..959528dd5361a 100644 --- a/x-pack/solutions/security/packages/features/src/types.ts +++ b/x-pack/solutions/security/packages/features/src/types.ts @@ -138,7 +138,10 @@ export interface SubFeatureReplacement { 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 */ - skipPrivilegeCopy?: boolean; + /** 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/sub_features.test.ts b/x-pack/solutions/security/packages/features/src/utils/sub_features.test.ts index 074ec8ffd2f43..34d938059096e 100644 --- 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 @@ -51,7 +51,7 @@ describe('sub_features', () => { const replacements: SubFeatureReplacement[] = [ { feature: 'replacementFeature', - skipPrivilegeCopy: false, + removeOriginalPrivileges: false, }, ]; @@ -69,11 +69,11 @@ describe('sub_features', () => { ]); }); - it('does not copy privilege IDs when skipPrivilegeCopy is true', () => { + it('does not copy privilege IDs when removeOriginalPrivileges is true', () => { const replacements: SubFeatureReplacement[] = [ { feature: 'replacementFeature', - skipPrivilegeCopy: true, + removeOriginalPrivileges: true, }, ]; @@ -89,7 +89,7 @@ describe('sub_features', () => { const replacements: SubFeatureReplacement[] = [ { feature: 'replacementFeature', - skipPrivilegeCopy: false, + removeOriginalPrivileges: false, additionalPrivileges: { privilege1: ['extraPriv1', 'extraPriv2'], privilege2: ['extraPriv3'], @@ -128,7 +128,7 @@ describe('sub_features', () => { const replacements: SubFeatureReplacement[] = [ { feature: 'newFeature', - skipPrivilegeCopy: false, + removeOriginalPrivileges: false, }, ]; @@ -145,11 +145,11 @@ describe('sub_features', () => { const replacements: SubFeatureReplacement[] = [ { feature: 'feature1', - skipPrivilegeCopy: false, + removeOriginalPrivileges: false, }, { feature: 'feature2', - skipPrivilegeCopy: true, + removeOriginalPrivileges: true, additionalPrivileges: { privilege1: ['extra1'], }, @@ -229,7 +229,7 @@ describe('sub_features', () => { const replacements: SubFeatureReplacement[] = [ { feature: 'replacementFeature', - skipPrivilegeCopy: false, + removeOriginalPrivileges: false, }, ]; @@ -261,7 +261,7 @@ describe('sub_features', () => { const replacements: SubFeatureReplacement[] = [ { feature: 'replacementFeature', - skipPrivilegeCopy: false, + removeOriginalPrivileges: false, }, ]; 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 index 4acb4ce84de7e..c37064bbe84cd 100644 --- a/x-pack/solutions/security/packages/features/src/utils/sub_features.ts +++ b/x-pack/solutions/security/packages/features/src/utils/sub_features.ts @@ -30,7 +30,7 @@ export const addSubFeatureReplacements = ( privilegeGroup.privileges.forEach((privilege) => { privilege.replacedBy ??= []; for (const replacement of replacements) { - const privileges = !replacement.skipPrivilegeCopy ? [privilege.id] : []; + const privileges = !replacement.removeOriginalPrivileges ? [privilege.id] : []; privileges.push(...(replacement.additionalPrivileges?.[privilege.id] ?? [])); privilege.replacedBy.push({ feature: replacement.feature, privileges }); }