From 4d405a079c90b244804526150affaa24ea4b61a7 Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Tue, 22 Jul 2025 13:02:25 +0200 Subject: [PATCH 01/26] feat: add trusted devices feature flag and role --- .../shared/fleet/common/constants/authz.ts | 12 ++++ .../features/src/product_features_keys.ts | 1 + .../v3_features/kibana_sub_features.ts | 72 +++++++++++++++++++ .../endpoint/service/authz/authz.test.ts | 10 ++- .../common/endpoint/service/authz/authz.ts | 6 ++ .../common/endpoint/types/authz.ts | 4 ++ .../common/experimental_features.ts | 6 ++ .../security_product_features_config.ts | 1 + .../security_product_features_config.ts | 1 + 9 files changed, 112 insertions(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/fleet/common/constants/authz.ts b/x-pack/platform/plugins/shared/fleet/common/constants/authz.ts index 290b86bf8edee..58a5bcb2307d7 100644 --- a/x-pack/platform/plugins/shared/fleet/common/constants/authz.ts +++ b/x-pack/platform/plugins/shared/fleet/common/constants/authz.ts @@ -60,6 +60,18 @@ export const ENDPOINT_PRIVILEGES: Record = deepFreez privilegeType: 'api', privilegeName: 'readTrustedApplications', }, + writeTrustedDevices: { + appId: DEFAULT_APP_CATEGORIES.security.id, + privilegeSplit: '-', + privilegeType: 'api', + privilegeName: 'writeTrustedDevices', + }, + readTrustedDevices: { + appId: DEFAULT_APP_CATEGORIES.security.id, + privilegeSplit: '-', + privilegeType: 'api', + privilegeName: 'readTrustedDevices', + }, writeHostIsolationExceptions: { appId: DEFAULT_APP_CATEGORIES.security.id, privilegeSplit: '-', 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..e6ba3f3a80f66 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 @@ -179,6 +179,7 @@ export enum SecuritySubFeatureId { endpointList = 'endpointListSubFeature', endpointExceptions = 'endpointExceptionsSubFeature', trustedApplications = 'trustedApplicationsSubFeature', + trustedDevices = 'trustedDevicesSubFeature', hostIsolationExceptionsBasic = 'hostIsolationExceptionsBasicSubFeature', blocklist = 'blocklistSubFeature', eventFilters = 'eventFiltersSubFeature', 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..f4202801e306b 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 @@ -137,6 +137,65 @@ const trustedApplicationsSubFeature = (): SubFeatureConfig => ({ }, ], }); + +const trustedDevicesSubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedDevices.privilegesTooltip', + { + defaultMessage: 'All Spaces is required for Trusted Devices access.', + } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedDevices', + { + defaultMessage: 'Trusted Devices', + } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedDevices.description', + { + defaultMessage: + 'Allows management of trusted USB and external devices that bypass device control protections.', + } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + api: [ + 'lists-all', + 'lists-read', + 'lists-summary', + `${APP_ID}-writeTrustedDevices`, + `${APP_ID}-readTrustedDevices`, + ], + id: 'trusted_devices_all', + includeIn: 'none', + name: TRANSLATIONS.all, + savedObject: { + all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC], + read: [], + }, + ui: ['writeTrustedDevices', 'readTrustedDevices'], + }, + { + api: ['lists-read', 'lists-summary', `${APP_ID}-readTrustedDevices`], + id: 'trusted_devices_read', + includeIn: 'none', + name: TRANSLATIONS.read, + savedObject: { + all: [], + read: [], + }, + ui: ['readTrustedDevices'], + }, + ], + }, + ], +}); + const hostIsolationExceptionsBasicSubFeature = (): SubFeatureConfig => ({ requireAllSpaces: true, privilegesTooltip: i18n.translate( @@ -852,6 +911,19 @@ export const getSecurityV3SubFeaturesMap = ({ ]); } + if (experimentalFeatures.trustedDevicesEnabled) { + // place between trusted applications and host isolation exceptions + const trustedAppsIndex = securitySubFeaturesList.findIndex( + ([id]) => id === SecuritySubFeatureId.trustedApplications + ); + if (trustedAppsIndex !== -1) { + securitySubFeaturesList.splice(trustedAppsIndex + 1, 0, [ + SecuritySubFeatureId.trustedDevices, + enableSpaceAwarenessIfNeeded(trustedDevicesSubFeature()), + ]); + } + } + const securitySubFeaturesMap = new Map( securitySubFeaturesList ); diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/authz/authz.test.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/authz/authz.test.ts index d7b631d6accb5..1c80989b3e163 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/authz/authz.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/authz/authz.test.ts @@ -169,6 +169,8 @@ describe('Endpoint Authz service', () => { ['canWriteFileOperations', 'writeFileOperations'], ['canWriteTrustedApplications', 'writeTrustedApplications'], ['canReadTrustedApplications', 'readTrustedApplications'], + ['canWriteTrustedDevices', 'writeTrustedDevices'], + ['canReadTrustedDevices', 'readTrustedDevices'], ['canWriteHostIsolationExceptions', 'writeHostIsolationExceptions'], ['canAccessHostIsolationExceptions', 'accessHostIsolationExceptions'], ['canReadHostIsolationExceptions', 'readHostIsolationExceptions'], @@ -211,6 +213,8 @@ describe('Endpoint Authz service', () => { ['canWriteFileOperations', ['writeFileOperations']], ['canWriteTrustedApplications', ['writeTrustedApplications']], ['canReadTrustedApplications', ['readTrustedApplications']], + ['canWriteTrustedDevices', ['writeTrustedDevices']], + ['canReadTrustedDevices', ['readTrustedDevices']], ['canWriteHostIsolationExceptions', ['writeHostIsolationExceptions']], ['canAccessHostIsolationExceptions', ['accessHostIsolationExceptions']], ['canReadHostIsolationExceptions', ['readHostIsolationExceptions']], @@ -264,6 +268,8 @@ describe('Endpoint Authz service', () => { ['canWriteFileOperations', ['writeFileOperations']], ['canWriteTrustedApplications', ['writeTrustedApplications']], ['canReadTrustedApplications', ['readTrustedApplications']], + ['canWriteTrustedDevices', ['writeTrustedDevices']], + ['canReadTrustedDevices', ['readTrustedDevices']], ['canWriteHostIsolationExceptions', ['writeHostIsolationExceptions']], ['canAccessHostIsolationExceptions', ['accessHostIsolationExceptions']], ['canReadHostIsolationExceptions', ['readHostIsolationExceptions']], @@ -361,8 +367,10 @@ describe('Endpoint Authz service', () => { canWriteFileOperations: false, canManageGlobalArtifacts: false, canWriteTrustedApplications: false, - canWriteWorkflowInsights: false, canReadTrustedApplications: false, + canWriteTrustedDevices: false, + canReadTrustedDevices: false, + canWriteWorkflowInsights: false, canReadWorkflowInsights: false, canWriteHostIsolationExceptions: false, canAccessHostIsolationExceptions: false, diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/authz/authz.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/authz/authz.ts index e4eb754e6bca9..d7d2f02cf34e2 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/authz/authz.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/authz/authz.ts @@ -82,6 +82,8 @@ export const calculateEndpointAuthz = ( const canWriteProcessOperations = hasAuth('writeProcessOperations'); const canWriteTrustedApplications = hasAuth('writeTrustedApplications'); const canReadTrustedApplications = hasAuth('readTrustedApplications'); + const canWriteTrustedDevices = hasAuth('writeTrustedDevices'); + const canReadTrustedDevices = hasAuth('readTrustedDevices'); const canWriteHostIsolationExceptions = hasAuth('writeHostIsolationExceptions'); const canReadHostIsolationExceptions = hasAuth('readHostIsolationExceptions'); const canAccessHostIsolationExceptions = hasAuth('accessHostIsolationExceptions'); @@ -153,6 +155,8 @@ export const calculateEndpointAuthz = ( // --------------------------------------------------------- canWriteTrustedApplications, canReadTrustedApplications, + canWriteTrustedDevices: canWriteTrustedDevices && isEnterpriseLicense, + canReadTrustedDevices: canReadTrustedDevices && isEnterpriseLicense, canWriteHostIsolationExceptions: canWriteHostIsolationExceptions && isPlatinumPlusLicense, canAccessHostIsolationExceptions: canAccessHostIsolationExceptions && isPlatinumPlusLicense, canReadHostIsolationExceptions, @@ -216,6 +220,8 @@ export const getEndpointAuthzInitialState = (): EndpointAuthz => { canWriteScanOperations: false, canWriteTrustedApplications: false, canReadTrustedApplications: false, + canWriteTrustedDevices: false, + canReadTrustedDevices: false, canWriteHostIsolationExceptions: false, canAccessHostIsolationExceptions: false, canReadHostIsolationExceptions: false, diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/authz.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/authz.ts index 262464aea0450..bdc25b8b1940a 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/authz.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/authz.ts @@ -64,6 +64,10 @@ export interface EndpointAuthz { canWriteTrustedApplications: boolean; /** If the user has read permissions for trusted applications */ canReadTrustedApplications: boolean; + /** If the user has write permissions for trusted devices */ + canWriteTrustedDevices: boolean; + /** If the user has read permissions for trusted devices */ + canReadTrustedDevices: boolean; /** If the user has write permissions for host isolation exceptions */ canWriteHostIsolationExceptions: boolean; /** If the user has read permissions for host isolation exceptions */ diff --git a/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts b/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts index e4057758ad1e9..d01c0c00e7a93 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts @@ -178,6 +178,12 @@ export const allowedExperimentalValues = Object.freeze({ */ unifiedManifestEnabled: true, + /** + * Enables Trusted Devices artifact management for device control protections. + * Allows users to manage trusted USB and external devices + */ + trustedDevicesEnabled: false, + /** * Enables the new modal for the value list items */ 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..5c8e09e047fb8 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 @@ -59,6 +59,7 @@ const securityProductFeaturesConfig: Record< subFeatureIds: [ SecuritySubFeatureId.hostIsolationExceptionsBasic, SecuritySubFeatureId.trustedApplications, + SecuritySubFeatureId.trustedDevices, SecuritySubFeatureId.blocklist, SecuritySubFeatureId.eventFilters, SecuritySubFeatureId.globalArtifactManagement, 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..d11176bc385c3 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 @@ -55,6 +55,7 @@ const securityProductFeaturesConfig: Record< subFeatureIds: [ SecuritySubFeatureId.hostIsolationExceptionsBasic, SecuritySubFeatureId.trustedApplications, + SecuritySubFeatureId.trustedDevices, SecuritySubFeatureId.blocklist, SecuritySubFeatureId.eventFilters, SecuritySubFeatureId.globalArtifactManagement, From 254e119c806072e00b9f4e8245cf6ee0934fc898 Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Tue, 22 Jul 2025 14:33:05 +0200 Subject: [PATCH 02/26] feat: add dedicated product feature key for Trusted Devices management --- .../security/packages/features/src/product_features_keys.ts | 5 +++++ .../packages/features/src/security/product_feature_config.ts | 4 ++++ .../product_features/security_product_features_config.ts | 1 - .../security_solution_serverless/common/pli/pli_config.ts | 1 + .../product_features/security_product_features_config.ts | 1 - 5 files changed, 10 insertions(+), 2 deletions(-) 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 e6ba3f3a80f66..42626660c04d1 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 @@ -30,6 +30,11 @@ export enum ProductFeatureSecurityKey { * running endpoint security */ endpointHostManagement = 'endpoint_host_management', + + /** + * Enables access to the Trusted Devices + */ + endpointTrustedDevices = 'endpoint_trusted_devices', /** * Enables access to Endpoint host isolation and release actions */ 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..651fb05b5902b 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 @@ -119,6 +119,10 @@ export const securityDefaultProductFeaturesConfig: DefaultSecurityProductFeature subFeatureIds: [SecuritySubFeatureId.endpointList], }, + [ProductFeatureSecurityKey.endpointTrustedDevices]: { + subFeatureIds: [SecuritySubFeatureId.trustedDevices], + }, + [ProductFeatureSecurityKey.endpointPolicyManagement]: { subFeatureIds: [SecuritySubFeatureId.policyManagement], }, 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 5c8e09e047fb8..955152b1c84fc 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 @@ -59,7 +59,6 @@ const securityProductFeaturesConfig: Record< subFeatureIds: [ SecuritySubFeatureId.hostIsolationExceptionsBasic, SecuritySubFeatureId.trustedApplications, - SecuritySubFeatureId.trustedDevices, SecuritySubFeatureId.blocklist, SecuritySubFeatureId.eventFilters, SecuritySubFeatureId.globalArtifactManagement, diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/common/pli/pli_config.ts b/x-pack/solutions/security/plugins/security_solution_serverless/common/pli/pli_config.ts index 1eb309d137e49..539fa0e098f59 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/common/pli/pli_config.ts +++ b/x-pack/solutions/security/plugins/security_solution_serverless/common/pli/pli_config.ts @@ -68,6 +68,7 @@ export const PLI_PRODUCT_FEATURES: PliProductFeatures = { ProductFeatureKey.endpointPolicyProtections, ProductFeatureKey.endpointArtifactManagement, ProductFeatureKey.endpointExceptions, + ProductFeatureKey.endpointTrustedDevices, ProductFeatureKey.endpointHostIsolationExceptions, ProductFeatureKey.endpointResponseActions, ProductFeatureKey.osqueryAutomatedResponseActions, 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 d11176bc385c3..4b4fe72a13ac1 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 @@ -55,7 +55,6 @@ const securityProductFeaturesConfig: Record< subFeatureIds: [ SecuritySubFeatureId.hostIsolationExceptionsBasic, SecuritySubFeatureId.trustedApplications, - SecuritySubFeatureId.trustedDevices, SecuritySubFeatureId.blocklist, SecuritySubFeatureId.eventFilters, SecuritySubFeatureId.globalArtifactManagement, From 298cffcd5973bc5dbbfc55682576ee99223810e8 Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Wed, 23 Jul 2025 11:56:08 +0200 Subject: [PATCH 03/26] feat: add USB device protection card with enterprise license check --- .../packages/upselling/service/types.ts | 1 + .../common/experimental_features.ts | 12 ++-- .../cards/usb_device_protection_card.tsx | 56 +++++++++++++++++ .../components/setting_locked_card.test.tsx | 36 ++++++++++- .../components/setting_locked_card.tsx | 32 +++++++--- .../hooks/use_get_device_control_component.ts | 13 ++++ .../policy_settings_form.tsx | 27 +++++++- .../endpoint_device_control.tsx | 61 +++++++++++++++++++ .../sections/endpoint_management/index.ts | 6 ++ .../public/upselling/upsellings.tsx | 6 ++ 10 files changed, 232 insertions(+), 18 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/usb_device_protection_card.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/hooks/use_get_device_control_component.ts create mode 100644 x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/endpoint_device_control.tsx diff --git a/x-pack/solutions/security/packages/upselling/service/types.ts b/x-pack/solutions/security/packages/upselling/service/types.ts index 02e7b19af4c03..f09b86c269397 100644 --- a/x-pack/solutions/security/packages/upselling/service/types.ts +++ b/x-pack/solutions/security/packages/upselling/service/types.ts @@ -16,6 +16,7 @@ export type UpsellingSectionId = | 'endpointPolicyProtections' | 'osquery_automated_response_actions' | 'endpoint_protection_updates' + | 'endpoint_device_control' | 'endpoint_agent_tamper_protection' | 'endpoint_custom_notification' | 'cloud_security_posture_integration_installation' diff --git a/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts b/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts index d01c0c00e7a93..977e1521e9862 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts @@ -178,12 +178,6 @@ export const allowedExperimentalValues = Object.freeze({ */ unifiedManifestEnabled: true, - /** - * Enables Trusted Devices artifact management for device control protections. - * Allows users to manage trusted USB and external devices - */ - trustedDevicesEnabled: false, - /** * Enables the new modal for the value list items */ @@ -280,6 +274,12 @@ export const allowedExperimentalValues = Object.freeze({ * Enables advanced mode for Trusted Apps creation and update */ trustedAppsAdvancedMode: false, + + /** + * Enables Trusted Devices artifact management for device control protections. + * Allows users to manage trusted USB and external devices + */ + trustedDevicesEnabled: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/usb_device_protection_card.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/usb_device_protection_card.tsx new file mode 100644 index 0000000000000..88be4eccf4d52 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/usb_device_protection_card.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import type { PolicyFormComponentCommonProps } from '../../types'; +import { useTestIdGenerator } from '../../../../../../hooks/use_test_id_generator'; +import { useLicense } from '../../../../../../../common/hooks/use_license'; +import { SettingLockedCard } from '../setting_locked_card'; + +export type UsbDeviceProtectionProps = PolicyFormComponentCommonProps; + +export const DEVICE_CONTROL_CARD_TITLE = i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.deviceControl', + { + defaultMessage: 'USB device protection', + } +); + +/** + * The Malware Protections form for policy details + * which will configure for all relevant OSes. + */ +export const DeviceControlCard = React.memo( + ({ policy, onChange, mode = 'edit', 'data-test-subj': dataTestSubj }) => { + const getTestId = useTestIdGenerator(dataTestSubj); + const isEnterprise = useLicense().isEnterprise(); + + // const shouldRenderComponent = isProtectionsAllowed && isTrustedDevicesAllowed && isEnterprise; + // const selected = (policy && policy.windows[protection].mode) !== ProtectionModes.off; + + // const protectionLabel = i18n.translate( + // 'xpack.securitySolution.endpoint.policy.protections.malware', + // { + // defaultMessage: 'Malware protections', + // } + // ); + + if (!isEnterprise) { + return ( + + ); + } + + return
{'Hello'}
; + } +); +DeviceControlCard.displayName = 'DeviceControlCard'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_locked_card.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_locked_card.test.tsx index 91e74d1f35d5a..d0032d940be61 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_locked_card.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_locked_card.test.tsx @@ -31,7 +31,7 @@ describe('Policy form SettingLockedCard component', () => { }; }); - it('should render with expected content', () => { + it('should render with expected content using default platinum license', () => { const { getByTestId } = render(); expect(getByTestId('test')).toHaveTextContent( @@ -46,4 +46,38 @@ describe('Policy form SettingLockedCard component', () => { ) ); }); + + it('should render with platinum license when explicitly specified', () => { + formProps.licenseType = 'platinum'; + const { getByTestId } = render(); + + expect(getByTestId('test')).toHaveTextContent( + exactMatchText( + 'Malware locked' + + 'Upgrade to Elastic Platinum' + + 'To turn on this protection, you must upgrade your license to Platinum, start a free 30-day ' + + 'trial, or spin up a ' + + 'cloud deployment' + + '(external, opens in a new tab or window) ' + + 'on AWS, GCP, or Azure.Platinum' + ) + ); + }); + + it('should render with enterprise license when specified', () => { + formProps.licenseType = 'enterprise'; + const { getByTestId } = render(); + + expect(getByTestId('test')).toHaveTextContent( + exactMatchText( + 'Malware locked' + + 'Upgrade to Elastic Enterprise' + + 'To turn on this protection, you must upgrade your license to Enterprise, start a free 30-day ' + + 'trial, or spin up a ' + + 'cloud deployment' + + '(external, opens in a new tab or window) ' + + 'on AWS, GCP, or Azure.Enterprise' + ) + ); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_locked_card.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_locked_card.tsx index 1adf53fe3c8b1..c581c09096c19 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_locked_card.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_locked_card.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { memo } from 'react'; +import React from 'react'; import { EuiCard, EuiIcon, @@ -16,8 +16,8 @@ import { EuiText, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; +import styled from 'styled-components'; import { useTestIdGenerator } from '../../../../../hooks/use_test_id_generator'; const LockedPolicyDiv = styled.div` @@ -33,22 +33,30 @@ const LockedPolicyDiv = styled.div` export interface SettingLockedCardProps { title: string; + licenseType?: 'platinum' | 'enterprise'; 'data-test-subj'?: string; } -export const SettingLockedCard = memo( - ({ title, 'data-test-subj': dataTestSubj }: SettingLockedCardProps) => { +export const SettingLockedCard = React.memo( + ({ title, licenseType = 'platinum', 'data-test-subj': dataTestSubj }: SettingLockedCardProps) => { const getTestId = useTestIdGenerator(dataTestSubj); + const licenseDisplayName = + licenseType === 'enterprise' + ? i18n.translate('xpack.securitySolution.endpoint.policy.details.enterprise', { + defaultMessage: 'Enterprise', + }) + : i18n.translate('xpack.securitySolution.endpoint.policy.details.platinum', { + defaultMessage: 'Platinum', + }); + return ( } @@ -65,8 +73,11 @@ export const SettingLockedCard = memo(

@@ -75,9 +86,10 @@ export const SettingLockedCard = memo(

{ + return useUpsellingComponent('endpoint_device_control'); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.tsx index 2bd23be6d22ee..526e9b4c3f201 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.tsx @@ -27,6 +27,8 @@ import { MalwareProtectionsCard } from './components/cards/malware_protections_c import type { PolicyFormComponentCommonProps } from './types'; import { AdvancedSection } from './components/advanced_section'; import { useTestIdGenerator } from '../../../../hooks/use_test_id_generator'; +import { useGetDeviceControlUpsellComponent } from './hooks/use_get_device_control_component'; +import { DeviceControlCard } from './components/cards/usb_device_protection_card'; const PROTECTIONS_SECTION_TITLE = i18n.translate( 'xpack.securitySolution.endpoint.policy.details.protections', @@ -43,10 +45,31 @@ export type PolicySettingsFormProps = PolicyFormComponentCommonProps; export const PolicySettingsForm = memo((props) => { const getTestId = useTestIdGenerator(props['data-test-subj']); const ProtectionsUpSellingComponent = useGetProtectionsUnavailableComponent(); + const DeviceControlUpSellingComponent = useGetDeviceControlUpsellComponent(); const { storage } = useKibana().services; - const { eventCollectionDataReductionBannerEnabled } = useEnableExperimental(); + const { eventCollectionDataReductionBannerEnabled, trustedDevicesEnabled } = + useEnableExperimental(); + + // Helper function to render trusted devices section + const renderDeviceControlSection = () => { + if (!trustedDevicesEnabled) { + return null; + } + + return ( + <> + {DeviceControlUpSellingComponent ? ( + + ) : ( + + )} + + + ); + }; + const [showEventMergingBanner, setShowEventMergingBanner] = useState( eventCollectionDataReductionBannerEnabled && (storage.get('securitySolution.showEventMergingBanner') ?? true) @@ -92,6 +115,8 @@ export const PolicySettingsForm = memo((props) => { /> + {renderDeviceControlSection()} + diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/endpoint_device_control.tsx b/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/endpoint_device_control.tsx new file mode 100644 index 0000000000000..ce04d7fffc972 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/endpoint_device_control.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { EuiCard, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import styled from '@emotion/styled'; + +const CARD_TITLE = i18n.translate( + 'xpack.securitySolutionServerless.endpointDeviceControl.cardTitle', + { + defaultMessage: 'USB device protection', + } +); +const CARD_MESSAGE = i18n.translate( + 'xpack.securitySolutionServerless.endpointDeviceControl.cardMessage', + { + defaultMessage: + 'To turn on USB device protection, you must add at least Endpoint Complete to your project. ', + } +); +const BADGE_TEXT = i18n.translate( + 'xpack.securitySolutionServerless.endpointDeviceControl.badgeText', + { + defaultMessage: 'Endpoint Complete', + } +); + +const CardDescription = styled.p` + padding: 0 33.3%; +`; + +/** + * Component displayed when a given product tier is not allowed to use endpoint policy protections. + */ +export const EndpointDeviceControl = memo(() => { + return ( + } + betaBadgeProps={{ + 'data-test-subj': 'endpointDeviceControlLockedCard-badge', + label: BADGE_TEXT, + }} + title={ +

+ {CARD_TITLE} +

+ } + > + {CARD_MESSAGE} +
+ ); +}); +EndpointDeviceControl.displayName = 'EndpointDeviceControl'; diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/index.ts b/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/index.ts index 6797cc0d29fbf..e1db335477bb0 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/index.ts +++ b/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/index.ts @@ -36,3 +36,9 @@ export const EndpointAgentTamperProtectionLazy = lazy(() => default: EndpointAgentTamperProtection, })) ); + +export const EndpointDeviceControlLazy = lazy(() => + import('./endpoint_device_control').then(({ EndpointDeviceControl }) => ({ + default: EndpointDeviceControl, + })) +); diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/upsellings.tsx b/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/upsellings.tsx index 6b4e6e4b7302f..a016db0514c80 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/upsellings.tsx +++ b/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/upsellings.tsx @@ -24,6 +24,7 @@ import { EndpointCustomNotificationLazy, EndpointPolicyProtectionsLazy, EndpointProtectionUpdatesLazy, + EndpointDeviceControlLazy, RuleDetailsEndpointExceptionsLazy, } from './sections/endpoint_management'; import { getProductTypeByPLI } from './hooks/use_product_type_by_pli'; @@ -128,6 +129,11 @@ export const upsellingSections: UpsellingSections = [ pli: ProductFeatureKey.endpointExceptions, component: RuleDetailsEndpointExceptionsLazy, }, + { + id: 'endpoint_device_control', + pli: ProductFeatureKey.endpointTrustedDevices, + component: EndpointDeviceControlLazy, + }, { id: 'endpoint_protection_updates', pli: ProductFeatureKey.endpointProtectionUpdates, From b231c5ff1b6c6292e772162e33a6aa2af7bca675 Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Wed, 23 Jul 2025 12:39:38 +0200 Subject: [PATCH 04/26] refactor: rename and update Device Control card component --- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../components/cards/device_control_card.tsx | 121 ++++++++++++++++++ .../cards/usb_device_protection_card.tsx | 56 -------- .../components/setting_locked_card.tsx | 3 +- .../policy_settings_form.tsx | 2 +- 7 files changed, 123 insertions(+), 62 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/device_control_card.tsx delete mode 100644 x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/usb_device_protection_card.tsx diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index 948d8df130f59..86f600f00c576 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -37393,7 +37393,6 @@ "xpack.securitySolution.endpoint.policy.details.updateErrorTitle": "Cette action a échoué !", "xpack.securitySolution.endpoint.policy.details.updateSuccessMessage": "L'intégration {name} a été mise à jour.", "xpack.securitySolution.endpoint.policy.details.updateSuccessTitle": "Cette action a réussi !", - "xpack.securitySolution.endpoint.policy.details.upgradeToPlatinum": "Mettre à niveau vers Elastic Platinum", "xpack.securitySolution.endpoint.policy.eventFilters.empty.unassigned.content": "Aucun filtre d'événement n'est actuellement affecté à {policyName}. Affectez des filtres d'événements maintenant, ou ajoutez-les et gérez-les sur la page des filtres d'événements.", "xpack.securitySolution.endpoint.policy.eventFilters.empty.unassigned.noPrivileges.content": "Aucun filtre d'événement n'est actuellement affecté à {policyName}", "xpack.securitySolution.endpoint.policy.eventFilters.empty.unassigned.primaryAction": "Affecter des filtres d'événements", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index e09f869551952..14e4725b9fc47 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -37433,7 +37433,6 @@ "xpack.securitySolution.endpoint.policy.details.updateErrorTitle": "失敗しました。", "xpack.securitySolution.endpoint.policy.details.updateSuccessMessage": "統合{name}が更新されました。", "xpack.securitySolution.endpoint.policy.details.updateSuccessTitle": "成功!", - "xpack.securitySolution.endpoint.policy.details.upgradeToPlatinum": "Elastic Platinum へのアップグレード", "xpack.securitySolution.endpoint.policy.eventFilters.empty.unassigned.content": "現在、{policyName}に割り当てられたイベントフィルターがありません。今するイベントフィルターを割り当てるか、イベントフィルターページで追加して管理してください。", "xpack.securitySolution.endpoint.policy.eventFilters.empty.unassigned.noPrivileges.content": "現在、{policyName}に割り当てられたイベントフィルターがありません", "xpack.securitySolution.endpoint.policy.eventFilters.empty.unassigned.primaryAction": "イベントフィルターの割り当て", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index 17d7d1b3b27df..5c87da171cb0b 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -37418,7 +37418,6 @@ "xpack.securitySolution.endpoint.policy.details.updateErrorTitle": "失败!", "xpack.securitySolution.endpoint.policy.details.updateSuccessMessage": "集成 {name} 已更新。", "xpack.securitySolution.endpoint.policy.details.updateSuccessTitle": "成功!", - "xpack.securitySolution.endpoint.policy.details.upgradeToPlatinum": "升级到 Elastic 白金级", "xpack.securitySolution.endpoint.policy.eventFilters.empty.unassigned.content": "当前没有事件筛选已分配给 {policyName}。立即分配事件筛选,或在事件筛选页面添加并管理事件筛选。", "xpack.securitySolution.endpoint.policy.eventFilters.empty.unassigned.noPrivileges.content": "当前没有事件筛选已分配给 {policyName}", "xpack.securitySolution.endpoint.policy.eventFilters.empty.unassigned.primaryAction": "分配事件筛选", diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/device_control_card.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/device_control_card.tsx new file mode 100644 index 0000000000000..8a3ab10bc026e --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/device_control_card.tsx @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { OperatingSystem } from '@kbn/securitysolution-utils'; +import { EuiSpacer } from '@elastic/eui'; +import type { PolicyFormComponentCommonProps } from '../../types'; +import { useTestIdGenerator } from '../../../../../../hooks/use_test_id_generator'; +import { useLicense } from '../../../../../../../common/hooks/use_license'; +import { SettingLockedCard } from '../setting_locked_card'; +import { SettingCard } from '../setting_card'; +import { ProtectionSettingCardSwitch } from '../protection_setting_card_switch'; +import { + PolicyOperatingSystem, + type Immutable, +} from '../../../../../../../../common/endpoint/types'; +import type { RansomwareProtectionOSes } from '../../../../types'; + +export type UsbDeviceProtectionProps = PolicyFormComponentCommonProps; + +export const DEVICE_CONTROL_CARD_TITLE = i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.deviceControl', + { + defaultMessage: 'Device Control', + } +); + +const protectionLabel = i18n.translate( + 'xpack.securitySolution.endpoint.policy.protections.deviceControl', + { + defaultMessage: 'Device Control', + } +); + +const DEVICE_CONTROL_OS_VALUES: Immutable = [ + PolicyOperatingSystem.windows, + // PolicyOperatingSystem.mac, +]; + +/** + * The Malware Protections form for policy details + * which will configure for all relevant OSes. + */ +export const DeviceControlCard = React.memo( + ({ policy, onChange, mode = 'edit', 'data-test-subj': dataTestSubj }) => { + const getTestId = useTestIdGenerator(dataTestSubj); + const isEnterprise = useLicense().isEnterprise(); + + // const shouldRenderComponent = isProtectionsAllowed && isTrustedDevicesAllowed && isEnterprise; + // const selected = (policy && policy.windows[protection].mode) !== ProtectionModes.off; + + // const protectionLabel = i18n.translate( + // 'xpack.securitySolution.endpoint.policy.protections.malware', + // { + // defaultMessage: 'Malware protections', + // } + // ); + + if (!isEnterprise) { + return ( + + ); + } + + return ( + + } + > + {/* + + */} + + + + ); + } +); +DeviceControlCard.displayName = 'DeviceControlCard'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/usb_device_protection_card.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/usb_device_protection_card.tsx deleted file mode 100644 index 88be4eccf4d52..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/usb_device_protection_card.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import type { PolicyFormComponentCommonProps } from '../../types'; -import { useTestIdGenerator } from '../../../../../../hooks/use_test_id_generator'; -import { useLicense } from '../../../../../../../common/hooks/use_license'; -import { SettingLockedCard } from '../setting_locked_card'; - -export type UsbDeviceProtectionProps = PolicyFormComponentCommonProps; - -export const DEVICE_CONTROL_CARD_TITLE = i18n.translate( - 'xpack.securitySolution.endpoint.policy.details.deviceControl', - { - defaultMessage: 'USB device protection', - } -); - -/** - * The Malware Protections form for policy details - * which will configure for all relevant OSes. - */ -export const DeviceControlCard = React.memo( - ({ policy, onChange, mode = 'edit', 'data-test-subj': dataTestSubj }) => { - const getTestId = useTestIdGenerator(dataTestSubj); - const isEnterprise = useLicense().isEnterprise(); - - // const shouldRenderComponent = isProtectionsAllowed && isTrustedDevicesAllowed && isEnterprise; - // const selected = (policy && policy.windows[protection].mode) !== ProtectionModes.off; - - // const protectionLabel = i18n.translate( - // 'xpack.securitySolution.endpoint.policy.protections.malware', - // { - // defaultMessage: 'Malware protections', - // } - // ); - - if (!isEnterprise) { - return ( - - ); - } - - return
{'Hello'}
; - } -); -DeviceControlCard.displayName = 'DeviceControlCard'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_locked_card.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_locked_card.tsx index c581c09096c19..5e9d7c82da587 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_locked_card.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_locked_card.tsx @@ -86,8 +86,7 @@ export const SettingLockedCard = React.memo(

Date: Fri, 25 Jul 2025 13:46:26 +0200 Subject: [PATCH 05/26] feat: add device control settings to endpoint security policy --- .../cards/device_control_card.test.tsx | 177 +++++++++++++++ ...device_control_notify_user_option.test.tsx | 190 ++++++++++++++++ .../device_control_notify_user_option.tsx | 203 +++++++++++++++++ .../device_control_protection_level.test.tsx | 132 +++++++++++ .../device_control_protection_level.tsx | 212 ++++++++++++++++++ ...evice_control_setting_card_switch.test.tsx | 208 +++++++++++++++++ .../device_control_setting_card_switch.tsx | 124 ++++++++++ .../components/shared_translations.ts | 38 ++++ 8 files changed, 1284 insertions(+) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/device_control_card.test.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_notify_user_option.test.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_notify_user_option.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.test.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_setting_card_switch.test.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_setting_card_switch.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/shared_translations.ts diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/device_control_card.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/device_control_card.test.tsx new file mode 100644 index 0000000000000..133e03198e96b --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/device_control_card.test.tsx @@ -0,0 +1,177 @@ +/* + * 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 { expectIsViewOnly, getPolicySettingsFormTestSubjects, exactMatchText } from '../../mocks'; +import type { AppContextTestRender } from '../../../../../../../common/mock/endpoint'; +import { createAppRootMockRenderer } from '../../../../../../../common/mock/endpoint'; +import { FleetPackagePolicyGenerator } from '../../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator'; +import React from 'react'; +import { DeviceControlAccessLevel } from '../../../../../../../../common/endpoint/types'; +import { set } from '@kbn/safer-lodash-set'; +import { createLicenseServiceMock } from '../../../../../../../../common/license/mocks'; +import { licenseService as licenseServiceMocked } from '../../../../../../../common/hooks/__mocks__/use_license'; +import { useLicense as _useLicense } from '../../../../../../../common/hooks/use_license'; +import type { DeviceControlProps } from './device_control_card'; +import { DEVICE_CONTROL_CARD_TITLE, DeviceControlCard } from './device_control_card'; + +jest.mock('../../../../../../../common/hooks/use_license'); + +const useLicenseMock = _useLicense as jest.Mock; + +describe('Policy Device Control Card', () => { + const testSubj = getPolicySettingsFormTestSubjects('test').deviceControl; + + let formProps: DeviceControlProps; + let render: () => ReturnType; + let renderResult: ReturnType; + + beforeEach(() => { + const mockedContext = createAppRootMockRenderer(); + + formProps = { + policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0] + .config.policy.value, + onChange: jest.fn(), + mode: 'edit', + 'data-test-subj': testSubj.card, + }; + + render = () => (renderResult = mockedContext.render()); + }); + + it('should render the card with expected components', () => { + const { getByTestId } = render(); + + expect(getByTestId(testSubj.enableDisableSwitch)); + expect(getByTestId(testSubj.protectionAuditRadio)); + expect(getByTestId(testSubj.notifyUserCheckbox)); + }); + + it('should show supported OS values', () => { + render(); + + expect(renderResult.getByTestId(testSubj.osValuesContainer)).toHaveTextContent( + exactMatchText('Windows, Mac') + ); + }); + + describe('and license is lower than Enterprise', () => { + beforeEach(() => { + const licenseServiceMock = createLicenseServiceMock(); + licenseServiceMock.isEnterprise.mockReturnValue(false); + + useLicenseMock.mockReturnValue(licenseServiceMock); + }); + + afterEach(() => { + useLicenseMock.mockReturnValue(licenseServiceMocked); + }); + + it('should show locked card if license not enterprise+', () => { + render(); + + expect(renderResult.getByTestId(testSubj.lockedCardTitle)).toHaveTextContent( + exactMatchText(DEVICE_CONTROL_CARD_TITLE) + ); + }); + }); + + describe('and displayed in View mode', () => { + beforeEach(() => { + formProps.mode = 'view'; + }); + + it('should display correctly when overall card is enabled', () => { + const { getByTestId } = render(); + + expectIsViewOnly(getByTestId(testSubj.card)); + + expect(getByTestId(testSubj.card)).toHaveTextContent( + exactMatchText( + 'Type' + + 'Device Control' + + 'Operating system' + + 'Windows, Mac ' + + 'Device Control' + + 'USB storage access level' + + 'Block all' + + 'User notification' + + 'Notify user' + + 'Notification message' + + '—' + ) + ); + expect(getByTestId(testSubj.enableDisableSwitch).getAttribute('aria-checked')).toBe('true'); + expect(getByTestId(testSubj.notifyUserCheckbox)).toHaveAttribute('checked'); + }); + + it('should display correctly when overall card is disabled', () => { + set(formProps.policy, 'windows.device_control.enabled', false); + set(formProps.policy, 'mac.device_control.enabled', false); + const { getByTestId } = render(); + + expectIsViewOnly(getByTestId(testSubj.card)); + + expect(getByTestId(testSubj.card)).toHaveTextContent( + exactMatchText( + ['Type', 'Device Control', 'Operating system', 'Windows, Mac ', 'Device Control'].join('') + ) + ); + expect(getByTestId(testSubj.enableDisableSwitch).getAttribute('aria-checked')).toBe('false'); + }); + + it('should display user notification disabled', () => { + set(formProps.policy, 'windows.popup.device_control.enabled', false); + set(formProps.policy, 'mac.popup.device_control.enabled', false); + + const { getByTestId } = render(); + + expectIsViewOnly(getByTestId(testSubj.card)); + + expect(getByTestId(testSubj.card)).toHaveTextContent( + exactMatchText( + 'Type' + + 'Device Control' + + 'Operating system' + + 'Windows, Mac ' + + 'Device Control' + + 'USB storage access level' + + 'Block all' + + 'User notification' + + 'Notify user' + ) + ); + expect(getByTestId(testSubj.enableDisableSwitch).getAttribute('aria-checked')).toBe('true'); + expect(getByTestId(testSubj.notifyUserCheckbox)).not.toHaveAttribute('checked'); + }); + + it('should display correctly with block protection level', () => { + set(formProps.policy, 'windows.device_control.access_level', DeviceControlAccessLevel.block); + set(formProps.policy, 'mac.device_control.access_level', DeviceControlAccessLevel.block); + + const { getByTestId } = render(); + + expectIsViewOnly(getByTestId(testSubj.card)); + + expect(getByTestId(testSubj.card)).toHaveTextContent( + exactMatchText( + 'Type' + + 'Device Control' + + 'Operating system' + + 'Windows, Mac ' + + 'Device Control' + + 'USB storage access level' + + 'Block all' + + 'User notification' + + 'Notify user' + + 'Notification message' + + '—' + ) + ); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_notify_user_option.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_notify_user_option.test.tsx new file mode 100644 index 0000000000000..5bca76203f9ff --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_notify_user_option.test.tsx @@ -0,0 +1,190 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { cloneDeep } from 'lodash'; +import userEvent from '@testing-library/user-event'; +import { useLicense as _useLicense } from '../../../../../../common/hooks/use_license'; +import type { AppContextTestRender } from '../../../../../../common/mock/endpoint'; +import { createAppRootMockRenderer } from '../../../../../../common/mock/endpoint'; +import { FleetPackagePolicyGenerator } from '../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator'; +import { createLicenseServiceMock } from '../../../../../../../common/license/mocks'; +import { licenseService as licenseServiceMocked } from '../../../../../../common/hooks/__mocks__/use_license'; +import { expectIsViewOnly, exactMatchText } from '../mocks'; +import { + NOTIFY_USER_SECTION_TITLE, + NOTIFY_USER_CHECKBOX_LABEL, + CUSTOMIZE_NOTIFICATION_MESSAGE_LABEL, +} from './shared_translations'; +import type { DeviceControlNotifyUserOptionProps } from './device_control_notify_user_option'; +import { DeviceControlNotifyUserOption } from './device_control_notify_user_option'; + +jest.mock('../../../../../../common/hooks/use_license'); + +const useLicenseMock = _useLicense as jest.Mock; + +describe('Policy form DeviceControlNotifyUserOption component', () => { + let formProps: DeviceControlNotifyUserOptionProps; + let render: () => ReturnType; + let renderResult: ReturnType; + + const isChecked = (selector: string): boolean => { + return (renderResult.getByTestId(selector) as HTMLInputElement).checked; + }; + + beforeEach(() => { + const mockedContext = createAppRootMockRenderer(); + const policy = new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0] + .config.policy.value; + + // Enable device control and notifications by default + policy.windows.device_control = { enabled: true, usb_storage: 'block' }; + policy.mac.device_control = { enabled: true, usb_storage: 'block' }; + policy.windows.popup.device_control = { enabled: true, message: 'hello world' }; + policy.mac.popup.device_control = { enabled: true, message: 'hello world' }; + + formProps = { + policy, + onChange: jest.fn(), + mode: 'edit', + 'data-test-subj': 'test', + }; + + render = () => { + renderResult = mockedContext.render(); + return renderResult; + }; + }); + + it('should render with expected content', () => { + const { getByTestId } = render(); + + expect(getByTestId('test')).toHaveTextContent(NOTIFY_USER_SECTION_TITLE); + expect(isChecked('test-checkbox')).toBe(true); + expect(renderResult.getByLabelText(NOTIFY_USER_CHECKBOX_LABEL)); + expect(getByTestId('test-customMessageTitle')).toHaveTextContent( + exactMatchText(CUSTOMIZE_NOTIFICATION_MESSAGE_LABEL) + ); + expect(getByTestId('test-customMessage')).toHaveValue('hello world'); + }); + + it('should render with options un-checked', () => { + formProps.policy.windows.popup.device_control!.enabled = false; + render(); + + expect(isChecked('test-checkbox')).toBe(false); + expect(renderResult.queryByTestId('test-customMessage')).toBeNull(); + }); + + it('should render checkbox disabled if device control is OFF', () => { + formProps.policy.windows.device_control!.enabled = false; + formProps.policy.mac.device_control!.enabled = false; + render(); + + expect(renderResult.getByTestId('test-checkbox')).toBeDisabled(); + }); + + it('should be able to un-check the option', async () => { + const expectedUpdatedPolicy = cloneDeep(formProps.policy); + expectedUpdatedPolicy.windows.popup.device_control!.enabled = false; + expectedUpdatedPolicy.mac.popup.device_control!.enabled = false; + + render(); + await userEvent.click(renderResult.getByTestId('test-checkbox')); + + expect(formProps.onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: expectedUpdatedPolicy, + }); + }); + + it('should be able to check the option', async () => { + formProps.policy.windows.popup.device_control!.enabled = false; + const expectedUpdatedPolicy = cloneDeep(formProps.policy); + expectedUpdatedPolicy.windows.popup.device_control!.enabled = true; + expectedUpdatedPolicy.mac.popup.device_control!.enabled = true; + + render(); + await userEvent.click(renderResult.getByTestId('test-checkbox')); + + expect(formProps.onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: expectedUpdatedPolicy, + }); + }); + + it('should be able to change the notification message', async () => { + const msg = 'a'; + // Set initial value to empty to avoid concatenation + formProps.policy.windows.popup.device_control!.message = ''; + formProps.policy.mac.popup.device_control!.message = ''; + const expectedUpdatedPolicy = cloneDeep(formProps.policy); + + render(); + const customMessageInput = renderResult.getByTestId('test-customMessage'); + await userEvent.clear(customMessageInput); + await userEvent.type(customMessageInput, msg); + + expectedUpdatedPolicy.windows.popup.device_control!.message = msg; + expectedUpdatedPolicy.mac.popup.device_control!.message = msg; + + expect(formProps.onChange).toHaveBeenCalledTimes(1); + expect(formProps.onChange).toHaveBeenLastCalledWith({ + isValid: true, + updatedPolicy: expectedUpdatedPolicy, + }); + }); + + describe('and license is lower than enterprise', () => { + beforeEach(() => { + const licenseServiceMock = createLicenseServiceMock(); + licenseServiceMock.isEnterprise.mockReturnValue(false); + useLicenseMock.mockReturnValue(licenseServiceMock); + }); + + afterEach(() => { + useLicenseMock.mockReturnValue(licenseServiceMocked); + }); + + it('should NOT render the component', () => { + render(); + expect(renderResult.queryByTestId('test')).toBeNull(); + }); + }); + + describe('and rendered in View mode', () => { + beforeEach(() => { + formProps.mode = 'view'; + }); + + it('should render with no form elements', () => { + render(); + expectIsViewOnly(renderResult.getByTestId('test')); + }); + + it('should render with expected output when checked', () => { + render(); + expect(renderResult.getByTestId('test')).toHaveTextContent( + 'User notificationNotify userNotification messagehello world' + ); + }); + + it('should render with expected output when checked with empty message', () => { + formProps.policy.windows.popup.device_control!.message = ''; + render(); + expect(renderResult.getByTestId('test')).toHaveTextContent( + 'User notificationNotify userNotification message—' + ); + }); + + it('should render with expected output when un-checked', () => { + formProps.policy.windows.popup.device_control!.enabled = false; + render(); + expect(renderResult.getByTestId('test')).toHaveTextContent('User notificationNotify user'); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_notify_user_option.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_notify_user_option.tsx new file mode 100644 index 0000000000000..55efa84c1581f --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_notify_user_option.tsx @@ -0,0 +1,203 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { cloneDeep } from 'lodash'; +import type { EuiCheckboxProps, EuiTextAreaProps } from '@elastic/eui'; +import { + EuiCheckbox, + EuiFlexGroup, + EuiFlexItem, + EuiIconTip, + EuiSpacer, + EuiText, + EuiTextArea, + EuiTitle, +} from '@elastic/eui'; +import { useTestIdGenerator } from '../../../../../hooks/use_test_id_generator'; +import { getEmptyValue } from '../../../../../../common/components/empty_value'; +import { useLicense } from '../../../../../../common/hooks/use_license'; +import { SettingCardHeader } from './setting_card'; +import type { PolicyFormComponentCommonProps } from '../types'; +import { useGetCustomNotificationUnavailableComponent } from '../hooks/use_get_custom_notification_unavailable_component'; +import { + NOTIFY_USER_SECTION_TITLE, + NOTIFY_USER_CHECKBOX_LABEL, + NOTIFICATION_MESSAGE_LABEL, + CUSTOMIZE_NOTIFICATION_MESSAGE_LABEL, +} from './shared_translations'; + +export type DeviceControlNotifyUserOptionProps = PolicyFormComponentCommonProps; + +export const DeviceControlNotifyUserOption = React.memo( + ({ + policy, + onChange, + mode, + 'data-test-subj': dataTestSubj, + }: DeviceControlNotifyUserOptionProps) => { + const isEnterprise = useLicense().isEnterprise(); + const getTestId = useTestIdGenerator(dataTestSubj); + const CustomNotificationUpsellingComponent = useGetCustomNotificationUnavailableComponent(); + + const isEditMode = mode === 'edit'; + + // Check if device control is enabled + const isDeviceControlEnabled = useMemo(() => { + return policy.windows.device_control?.enabled || policy.mac.device_control?.enabled || false; + }, [policy]); + + const userNotificationSelected = policy.windows.popup.device_control?.enabled || false; + const userNotificationMessage = policy.windows.popup.device_control?.message || ''; + + const handleUserNotificationCheckbox = useCallback( + (event) => { + const newPayload = cloneDeep(policy); + + // Update Windows popup device control + newPayload.windows.popup.device_control = newPayload.windows.popup.device_control || { + enabled: event.target.checked, + message: 'Elastic Security {action} {rule}', + }; + newPayload.windows.popup.device_control.enabled = event.target.checked; + + // Update Mac popup device control + newPayload.mac.popup.device_control = newPayload.mac.popup.device_control || { + enabled: event.target.checked, + message: 'Elastic Security {action} {rule}', + }; + newPayload.mac.popup.device_control.enabled = event.target.checked; + + onChange({ isValid: true, updatedPolicy: newPayload }); + }, + [policy, onChange] + ); + + const handleCustomUserNotification = useCallback>( + (event) => { + const newPayload = cloneDeep(policy); + // Update Windows popup device control message + newPayload.windows.popup.device_control = newPayload.windows.popup.device_control || { + enabled: false, + message: event.target.value, + }; + newPayload.windows.popup.device_control.message = event.target.value; + + // Update Mac popup device control message + newPayload.mac.popup.device_control = newPayload.mac.popup.device_control || { + enabled: false, + message: event.target.value, + }; + newPayload.mac.popup.device_control.message = event.target.value; + + onChange({ isValid: true, updatedPolicy: newPayload }); + }, + [policy, onChange] + ); + + const customNotificationComponent = useMemo(() => { + if (!userNotificationSelected) { + return null; + } + + if (CustomNotificationUpsellingComponent) { + return ; + } + + if (!isEditMode) { + return ( + <> + + +

{NOTIFICATION_MESSAGE_LABEL}

+ + + <>{userNotificationMessage || getEmptyValue()} + + ); + } + + return ( + <> + + + + +

{CUSTOMIZE_NOTIFICATION_MESSAGE_LABEL}

+
+
+ + + + + + + } + /> + +
+ + + + ); + }, [ + CustomNotificationUpsellingComponent, + getTestId, + handleCustomUserNotification, + isEditMode, + userNotificationMessage, + userNotificationSelected, + ]); + + if (!isEnterprise) { + return null; + } + + return ( +
+ + + +
{NOTIFY_USER_SECTION_TITLE}
+
+
+ + + + + + {customNotificationComponent} +
+ ); + } +); +DeviceControlNotifyUserOption.displayName = 'DeviceControlNotifyUserOption'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.test.tsx new file mode 100644 index 0000000000000..f0861c1258922 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.test.tsx @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { cloneDeep } from 'lodash'; +import userEvent from '@testing-library/user-event'; +import type { AppContextTestRender } from '../../../../../../common/mock/endpoint'; +import { createAppRootMockRenderer } from '../../../../../../common/mock/endpoint'; +import { FleetPackagePolicyGenerator } from '../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator'; +import { DeviceControlAccessLevel as DeviceControlAccessLevelEnum } from '../../../../../../../common/endpoint/types'; +import { expectIsViewOnly } from '../mocks'; +import type { DeviceControlProtectionLevelProps } from './device_control_protection_level'; +import { DeviceControlProtectionLevel } from './device_control_protection_level'; + +describe('Policy form DeviceControlProtectionLevel component', () => { + let formProps: DeviceControlProtectionLevelProps; + let render: () => ReturnType; + let renderResult: ReturnType; + + const clickAccessLevel = async (level: 'audit' | 'block') => { + await userEvent.click(renderResult.getByTestId(`test-${level}Radio`).querySelector('label')!); + }; + + const isAccessLevelChecked = (level: 'audit' | 'block'): boolean => { + return renderResult.getByTestId(`test-${level}Radio`)!.querySelector('input')!.checked ?? false; + }; + + beforeEach(() => { + const mockedContext = createAppRootMockRenderer(); + const policy = new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0] + .config.policy.value; + + // Default to block for tests + policy.windows.device_control = { + enabled: true, + usb_storage: DeviceControlAccessLevelEnum.block, + }; + policy.mac.device_control = { + enabled: true, + usb_storage: DeviceControlAccessLevelEnum.block, + }; + + formProps = { + policy, + onChange: jest.fn(), + mode: 'edit', + 'data-test-subj': 'test', + osList: ['windows', 'mac'], + }; + + render = () => { + renderResult = mockedContext.render(); + return renderResult; + }; + }); + + it('should render expected options', () => { + const { getByTestId } = render(); + + expect(getByTestId('test-auditRadio')); + expect(getByTestId('test-blockRadio')); + }); + + it('should allow audit mode to be selected', async () => { + const expectedPolicyUpdate = cloneDeep(formProps.policy); + expectedPolicyUpdate.windows.device_control!.usb_storage = DeviceControlAccessLevelEnum.audit; + expectedPolicyUpdate.mac.device_control!.usb_storage = DeviceControlAccessLevelEnum.audit; + + render(); + + expect(isAccessLevelChecked('block')).toBe(true); + + await clickAccessLevel('audit'); + + expect(formProps.onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: expectedPolicyUpdate, + }); + }); + + it('should allow block mode to be selected', async () => { + formProps.policy.windows.device_control!.usb_storage = DeviceControlAccessLevelEnum.audit; + formProps.policy.mac.device_control!.usb_storage = DeviceControlAccessLevelEnum.audit; + + const expectedPolicyUpdate = cloneDeep(formProps.policy); + expectedPolicyUpdate.windows.device_control!.usb_storage = DeviceControlAccessLevelEnum.block; + expectedPolicyUpdate.mac.device_control!.usb_storage = DeviceControlAccessLevelEnum.block; + + render(); + + expect(isAccessLevelChecked('audit')).toBe(true); + + await clickAccessLevel('block'); + + expect(formProps.onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: expectedPolicyUpdate, + }); + }); + + describe('and rendered in View mode', () => { + beforeEach(() => { + formProps.mode = 'view'; + }); + + it('should display block', () => { + render(); + + expectIsViewOnly(renderResult.getByTestId('test')); + expect(renderResult.getByTestId('test')).toHaveTextContent('Block all'); + }); + + it('should display audit', () => { + formProps.policy.windows.device_control!.usb_storage = DeviceControlAccessLevelEnum.audit; + render(); + + expectIsViewOnly(renderResult.getByTestId('test')); + expect(renderResult.getByTestId('test')).toHaveTextContent('Allow Read and Write'); + }); + + it('should not render radio buttons', () => { + render(); + + expect(renderResult.queryByTestId('test-auditRadio')).toBeNull(); + expect(renderResult.queryByTestId('test-blockRadio')).toBeNull(); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.tsx new file mode 100644 index 0000000000000..5bc471af04f20 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.tsx @@ -0,0 +1,212 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useCallback, useMemo } from 'react'; +import { cloneDeep } from 'lodash'; +import type { EuiFlexItemProps } from '@elastic/eui'; +import { EuiRadio, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useTestIdGenerator } from '../../../../../hooks/use_test_id_generator'; +import { SettingCardHeader } from './setting_card'; +import type { PolicyFormComponentCommonProps } from '../types'; +import type { + ImmutableArray, + Immutable, + DeviceControlAccessLevel, +} from '../../../../../../../common/endpoint/types'; +import { DeviceControlAccessLevel as DeviceControlAccessLevelEnum } from '../../../../../../../common/endpoint/types'; +import type { DeviceControlOSes } from '../../../types'; + +const AUDIT_LABEL = i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.deviceControl.allowReadWrite', + { + defaultMessage: 'Allow Read and Write', + } +); + +const BLOCK_LABEL = i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.deviceControl.blockAll', + { + defaultMessage: 'Block all', + } +); + +export type DeviceControlProtectionLevelProps = PolicyFormComponentCommonProps & { + osList: ImmutableArray; +}; + +export const DeviceControlProtectionLevel = memo( + ({ policy, osList, mode, onChange, 'data-test-subj': dataTestSubj }) => { + const isEditMode = mode === 'edit'; + const getTestId = useTestIdGenerator(dataTestSubj); + + const radios: Immutable< + Array<{ + id: DeviceControlAccessLevel; + label: string; + flexGrow: EuiFlexItemProps['grow']; + }> + > = useMemo(() => { + return [ + { + id: DeviceControlAccessLevelEnum.audit, + label: AUDIT_LABEL, + flexGrow: 1, + }, + { + id: DeviceControlAccessLevelEnum.block, + label: BLOCK_LABEL, + flexGrow: 5, + }, + ]; + }, []); + + const getCurrentAccessLevel = useMemo(() => { + // Check Windows first, then Mac for device_control configuration + if (policy.windows.device_control?.usb_storage) { + return policy.windows.device_control.usb_storage; + } + if (policy.mac.device_control?.usb_storage) { + return policy.mac.device_control.usb_storage; + } + return DeviceControlAccessLevelEnum.audit; + }, [policy]); + + const currentAccessLevelLabel = useMemo(() => { + const radio = radios.find((item) => item.id === getCurrentAccessLevel); + + if (radio) { + return radio.label; + } + + return BLOCK_LABEL; + }, [getCurrentAccessLevel, radios]); + + const isDeviceControlEnabled = useMemo(() => { + // Check if device control is enabled on any OS + return osList.some((os) => { + if (os === 'windows' || os === 'mac') { + return policy[os].device_control?.enabled; + } + return false; + }); + }, [policy, osList]); + + return ( +
+ + + + + + {isEditMode ? ( + radios.map(({ label, id, flexGrow }) => { + return ( + + + + ); + }) + ) : ( + <>{currentAccessLevelLabel} + )} + +
+ ); + } +); +DeviceControlProtectionLevel.displayName = 'DeviceControlProtectionLevel'; + +interface DeviceControlAccessRadioProps extends PolicyFormComponentCommonProps { + accessLevel: DeviceControlAccessLevel; + osList: ImmutableArray; + label: string; + isEnabled: boolean; +} + +const DeviceControlAccessRadio = React.memo( + ({ + accessLevel, + osList, + label, + isEnabled, + onChange, + policy, + mode, + 'data-test-subj': dataTestSubj, + }: DeviceControlAccessRadioProps) => { + const getCurrentAccessLevel = () => { + // Check Windows first, then Mac for device_control configuration + if (policy.windows.device_control?.usb_storage) { + return policy.windows.device_control.usb_storage; + } + if (policy.mac.device_control?.usb_storage) { + return policy.mac.device_control.usb_storage; + } + return DeviceControlAccessLevelEnum.audit; + }; + + const selected = getCurrentAccessLevel(); + const showEditableFormFields = mode === 'edit'; + + const radioId = useMemo(() => { + return `${osList.join('-')}-device_control-${accessLevel}`; + }, [osList, accessLevel]); + + const handleRadioChange = useCallback(() => { + const newPayload = cloneDeep(policy); + + // Update Windows device control + if (!newPayload.windows.device_control) { + newPayload.windows.device_control = { + enabled: true, + usb_storage: accessLevel, + }; + } else { + newPayload.windows.device_control.usb_storage = accessLevel; + } + + // Update Mac device control + if (!newPayload.mac.device_control) { + newPayload.mac.device_control = { + enabled: true, + usb_storage: accessLevel, + }; + } else { + newPayload.mac.device_control.usb_storage = accessLevel; + } + + onChange({ isValid: true, updatedPolicy: newPayload }); + }, [accessLevel, onChange, policy]); + + return ( + + ); + } +); + +DeviceControlAccessRadio.displayName = 'DeviceControlAccessRadio'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_setting_card_switch.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_setting_card_switch.test.tsx new file mode 100644 index 0000000000000..80f271968d48c --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_setting_card_switch.test.tsx @@ -0,0 +1,208 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { cloneDeep } from 'lodash'; +import userEvent from '@testing-library/user-event'; +import type { AppContextTestRender } from '../../../../../../common/mock/endpoint'; +import { createAppRootMockRenderer } from '../../../../../../common/mock/endpoint'; +import { FleetPackagePolicyGenerator } from '../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator'; +import type { PolicyConfig } from '../../../../../../../common/endpoint/types'; +import { DeviceControlAccessLevel as DeviceControlAccessLevelEnum } from '../../../../../../../common/endpoint/types'; +import { exactMatchText, expectIsViewOnly } from '../mocks'; +import type { DeviceControlSettingCardSwitchProps } from './device_control_setting_card_switch'; +import { DeviceControlSettingCardSwitch } from './device_control_setting_card_switch'; + +const setDeviceControlMode = ({ + policy, + turnOff = false, +}: { + policy: PolicyConfig; + turnOff?: boolean; +}) => { + const enabled = !turnOff; + + // Ensure popup and device_control objects exist and assign to constants + policy.windows.popup = policy.windows.popup ?? {}; + policy.mac.popup = policy.mac.popup ?? {}; + if (!policy.windows.device_control) { + policy.windows.device_control = { + enabled: false, + usb_storage: DeviceControlAccessLevelEnum.audit, + }; + } + if (!policy.mac.device_control) { + policy.mac.device_control = { + enabled: false, + usb_storage: DeviceControlAccessLevelEnum.audit, + }; + } + const windowsDeviceControl = policy.windows.device_control; + const macDeviceControl = policy.mac.device_control; + + // This logic now mirrors the component's behavior + windowsDeviceControl.enabled = enabled; + macDeviceControl.enabled = enabled; + + // When enabling, we set a default `usb_storage` level + if (enabled) { + windowsDeviceControl.usb_storage = DeviceControlAccessLevelEnum.block; + macDeviceControl.usb_storage = DeviceControlAccessLevelEnum.block; + } + + // Popups are always aligned with the enabled state + policy.windows.popup.device_control = { enabled, message: '' }; + policy.mac.popup.device_control = { enabled, message: '' }; +}; + +describe('Policy form DeviceControlSettingCardSwitch component', () => { + let formProps: DeviceControlSettingCardSwitchProps; + let render: () => ReturnType; + let renderResult: ReturnType; + + beforeEach(() => { + const mockedContext = createAppRootMockRenderer(); + const policy = new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0] + .config.policy.value; + + // Ensure device control is enabled by default for tests + setDeviceControlMode({ policy, turnOff: false }); + + formProps = { + policy, + onChange: jest.fn(), + mode: 'edit', + 'data-test-subj': 'test', + selected: true, + protectionLabel: 'Device Control', + osList: ['windows', 'mac'], + }; + + render = () => { + const selected = formProps.policy.windows.device_control?.enabled ?? false; + renderResult = mockedContext.render( + + ); + return renderResult; + }; + }); + + it('should render expected output when enabled', () => { + const { getByTestId } = render(); + + expect(getByTestId('test')).toHaveAttribute('aria-checked', 'true'); + expect(getByTestId('test-label')).toHaveTextContent(exactMatchText('Device Control')); + }); + + it('should render expected output when disabled', () => { + setDeviceControlMode({ policy: formProps.policy, turnOff: true }); + const { getByTestId } = render(); + + expect(getByTestId('test')).toHaveAttribute('aria-checked', 'false'); + expect(getByTestId('test-label')).toHaveTextContent(exactMatchText('Device Control')); + }); + + it('should be able to disable it', async () => { + const expectedUpdatedPolicy = cloneDeep(formProps.policy); + setDeviceControlMode({ policy: expectedUpdatedPolicy, turnOff: true }); + + render(); + await userEvent.click(renderResult.getByTestId('test')); + + expect(formProps.onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: expectedUpdatedPolicy, + }); + }); + + it('should be able to enable it', async () => { + // Start with it disabled + setDeviceControlMode({ policy: formProps.policy, turnOff: true }); + + const expectedUpdatedPolicy = cloneDeep(formProps.policy); + setDeviceControlMode({ policy: expectedUpdatedPolicy, turnOff: false }); + + render(); + await userEvent.click(renderResult.getByTestId('test')); + + expect(formProps.onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: expectedUpdatedPolicy, + }); + }); + + it('should invoke `additionalOnSwitchChange` callback if one was defined', async () => { + formProps.additionalOnSwitchChange = jest.fn(({ policyConfigData }) => { + const updated = cloneDeep(policyConfigData); + if (updated.windows.popup.device_control) { + updated.windows.popup.device_control.message = 'foo'; + } + return updated; + }); + + const expectedPolicyDataBeforeAdditionalCallback = cloneDeep(formProps.policy); + setDeviceControlMode({ policy: expectedPolicyDataBeforeAdditionalCallback, turnOff: true }); + + const expectedUpdatedPolicy = cloneDeep(expectedPolicyDataBeforeAdditionalCallback); + if (expectedUpdatedPolicy.windows.popup.device_control) { + expectedUpdatedPolicy.windows.popup.device_control.message = 'foo'; + } + + render(); + await userEvent.click(renderResult.getByTestId('test')); + + expect(formProps.additionalOnSwitchChange).toHaveBeenCalledWith({ + value: false, + policyConfigData: expectedPolicyDataBeforeAdditionalCallback, + protectionOsList: formProps.osList, + }); + + expect(formProps.onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: expectedUpdatedPolicy, + }); + }); + + describe('and rendered in View mode', () => { + beforeEach(() => { + formProps.mode = 'view'; + }); + + it('should not include any enabled form elements', () => { + render(); + + expectIsViewOnly(renderResult.getByTestId('test')); + }); + + it('should show option when checked', () => { + render(); + + expect(renderResult.getByTestId('test-label')).toHaveTextContent( + exactMatchText('Device Control') + ); + expect(renderResult.getByTestId('test').getAttribute('aria-checked')).toBe('true'); + }); + + it('should show option when unchecked', () => { + setDeviceControlMode({ policy: formProps.policy, turnOff: true }); + render(); + + expect(renderResult.getByTestId('test-label')).toHaveTextContent( + exactMatchText('Device Control') + ); + expect(renderResult.getByTestId('test').getAttribute('aria-checked')).toBe('false'); + }); + + it('should not be clickable', async () => { + render(); + + await userEvent.click(renderResult.getByTestId('test')); + + expect(formProps.onChange).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_setting_card_switch.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_setting_card_switch.tsx new file mode 100644 index 0000000000000..3472eb4afac77 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_setting_card_switch.tsx @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import type { EuiSwitchProps } from '@elastic/eui'; +import { EuiSwitch } from '@elastic/eui'; +import { cloneDeep } from 'lodash'; +import { useTestIdGenerator } from '../../../../../hooks/use_test_id_generator'; +import type { PolicyFormComponentCommonProps } from '../types'; +import type { ImmutableArray, PolicyConfig } from '../../../../../../../common/endpoint/types'; +import { DeviceControlAccessLevel as DeviceControlAccessLevelEnum } from '../../../../../../../common/endpoint/types'; +import type { DeviceControlOSes } from '../../../types'; + +export interface DeviceControlSettingCardSwitchProps extends PolicyFormComponentCommonProps { + selected: boolean; + protectionLabel?: string; + osList: ImmutableArray; + additionalOnSwitchChange?: ({ + value, + policyConfigData, + protectionOsList, + }: { + value: boolean; + policyConfigData: PolicyConfig; + protectionOsList: ImmutableArray; + }) => PolicyConfig; +} + +export const DeviceControlSettingCardSwitch = React.memo( + ({ + protectionLabel, + osList, + additionalOnSwitchChange, + onChange, + policy, + mode, + selected, + 'data-test-subj': dataTestSubj, + }: DeviceControlSettingCardSwitchProps) => { + const getTestId = useTestIdGenerator(dataTestSubj); + const isEditMode = mode === 'edit'; + + const handleSwitchChange = useCallback( + (event) => { + const newPayload = cloneDeep(policy); + + if (event.target.checked === false) { + // Disable device control for Windows and Mac + newPayload.windows.device_control = newPayload.windows.device_control || { + enabled: false, + usb_storage: DeviceControlAccessLevelEnum.audit, + }; + newPayload.windows.device_control.enabled = false; + newPayload.windows.popup.device_control = newPayload.windows.popup.device_control || { + enabled: false, + message: 'Elastic Security {action} {rule}', + }; + newPayload.windows.popup.device_control.enabled = false; + + newPayload.mac.device_control = newPayload.mac.device_control || { + enabled: false, + usb_storage: DeviceControlAccessLevelEnum.audit, + }; + newPayload.mac.device_control.enabled = false; + newPayload.mac.popup.device_control = newPayload.mac.popup.device_control || { + enabled: false, + message: 'Elastic Security {action} {rule}', + }; + newPayload.mac.popup.device_control.enabled = false; + } else { + // Enable device control for Windows and Mac + newPayload.windows.device_control = { + enabled: true, + usb_storage: DeviceControlAccessLevelEnum.block, + }; + newPayload.windows.popup.device_control = newPayload.windows.popup.device_control || { + enabled: true, + message: 'Elastic Security {action} {rule}', + }; + newPayload.windows.popup.device_control.enabled = true; + + newPayload.mac.device_control = { + enabled: true, + usb_storage: DeviceControlAccessLevelEnum.block, + }; + newPayload.mac.popup.device_control = newPayload.mac.popup.device_control || { + enabled: true, + message: 'Elastic Security {action} {rule}', + }; + newPayload.mac.popup.device_control.enabled = true; + } + + onChange({ + isValid: true, + updatedPolicy: additionalOnSwitchChange + ? additionalOnSwitchChange({ + value: event.target.checked, + policyConfigData: newPayload, + protectionOsList: osList, + }) + : newPayload, + }); + }, + [policy, onChange, additionalOnSwitchChange, osList] + ); + + return ( + + ); + } +); + +DeviceControlSettingCardSwitch.displayName = 'DeviceControlSettingCardSwitch'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/shared_translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/shared_translations.ts new file mode 100644 index 0000000000000..d80180cb8c1c4 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/shared_translations.ts @@ -0,0 +1,38 @@ +/* + * 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'; + +/** + * Shared translations for user notification components across different protection types + */ + +export const NOTIFY_USER_SECTION_TITLE = i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.userNotification', + { defaultMessage: 'User notification' } +); + +export const NOTIFY_USER_CHECKBOX_LABEL = i18n.translate( + 'xpack.securitySolution.endpoint.policyDetail.notifyUser', + { + defaultMessage: 'Notify user', + } +); + +export const NOTIFICATION_MESSAGE_LABEL = i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.notificationMessage', + { + defaultMessage: 'Notification message', + } +); + +export const CUSTOMIZE_NOTIFICATION_MESSAGE_LABEL = i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.customizeUserNotification', + { + defaultMessage: 'Customize notification message', + } +); From 94203534427478df297115ae6eec3802bcd15f5a Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Fri, 25 Jul 2025 13:46:47 +0200 Subject: [PATCH 06/26] feat: add device control settings to endpoint policy configuration --- .../common/endpoint/models/policy_config.ts | 48 ++++- .../models/policy_config_helpers.test.ts | 119 +++++++++++++ .../endpoint/models/policy_config_helpers.ts | 72 +++++++- .../common/endpoint/types/index.ts | 34 +++- .../policy/store/policy_details/index.test.ts | 16 ++ .../middleware/policy_settings_middleware.ts | 8 + .../selectors/policy_settings_selectors.ts | 2 + .../public/management/pages/policy/types.ts | 6 + .../components/cards/device_control_card.tsx | 64 +++---- .../components/notify_user_option.test.tsx | 12 +- .../components/notify_user_option.tsx | 32 +--- .../policy/view/policy_settings_form/mocks.ts | 10 ++ .../policy_settings_form.tsx | 2 +- .../endpoint/endpoint_app_context_services.ts | 11 +- .../fleet_integration.test.ts | 166 ++++++++++++++++-- .../fleet_integration/fleet_integration.ts | 18 +- .../handlers/create_default_policy.test.ts | 96 +++++++++- .../handlers/create_default_policy.ts | 12 +- 18 files changed, 637 insertions(+), 91 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/models/policy_config.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/models/policy_config.ts index 7884f512ba76b..1132034122ad4 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/models/policy_config.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/models/policy_config.ts @@ -6,7 +6,7 @@ */ import type { PolicyConfig } from '../types'; -import { AntivirusRegistrationModes, ProtectionModes } from '../types'; +import { AntivirusRegistrationModes, DeviceControlAccessLevel, ProtectionModes } from '../types'; import { isBillablePolicy } from './policy_config_helpers'; @@ -44,6 +44,10 @@ export const policyFactory = ({ registry: true, security: true, }, + device_control: { + enabled: true, + usb_storage: DeviceControlAccessLevel.block, + }, malware: { mode: ProtectionModes.prevent, blocklist: true, @@ -79,6 +83,10 @@ export const policyFactory = ({ message: '', enabled: true, }, + device_control: { + message: '', + enabled: true, + }, }, logging: { file: 'info', @@ -106,6 +114,10 @@ export const policyFactory = ({ blocklist: true, on_write_scan: true, }, + device_control: { + enabled: true, + usb_storage: DeviceControlAccessLevel.block, + }, behavior_protection: { mode: ProtectionModes.prevent, reputation_service: cloud, // Defaults to true if on cloud @@ -128,6 +140,10 @@ export const policyFactory = ({ message: '', enabled: true, }, + device_control: { + message: '', + enabled: true, + }, }, logging: { file: 'info', @@ -195,6 +211,20 @@ export const policyFactoryWithoutPaidEnterpriseFeatures = ( return { ...policy, global_manifest_version: 'latest', + windows: { + ...policy.windows, + device_control: { + enabled: false, + usb_storage: DeviceControlAccessLevel.audit, + }, + }, + mac: { + ...policy.mac, + device_control: { + enabled: false, + usb_storage: DeviceControlAccessLevel.audit, + }, + }, }; }; @@ -250,6 +280,10 @@ export const policyFactoryWithoutPaidFeatures = ( enabled: false, }, }, + device_control: { + enabled: false, + usb_storage: DeviceControlAccessLevel.audit, + }, popup: { ...policy.windows.popup, malware: { @@ -268,6 +302,10 @@ export const policyFactoryWithoutPaidFeatures = ( message: '', enabled: false, }, + device_control: { + message: '', + enabled: false, + }, }, }, mac: { @@ -281,6 +319,10 @@ export const policyFactoryWithoutPaidFeatures = ( mode: ProtectionModes.off, supported: false, }, + device_control: { + enabled: false, + usb_storage: DeviceControlAccessLevel.audit, + }, popup: { ...policy.mac.popup, malware: { @@ -295,6 +337,10 @@ export const policyFactoryWithoutPaidFeatures = ( message: '', enabled: false, }, + device_control: { + message: '', + enabled: false, + }, }, }, linux: { diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts index c1c3af673b2ef..6ce1921ac563d 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts @@ -16,6 +16,7 @@ import { getPolicyProtectionsReference, checkIfPopupMessagesContainCustomNotifications, resetCustomNotifications, + removeDeviceControl, } from './policy_config_helpers'; import { get, merge } from 'lodash'; import { set } from '@kbn/safer-lodash-set'; @@ -329,6 +330,120 @@ describe('Policy Config helpers', () => { ); }); }); + + describe('removeDeviceControl', () => { + let policy: PolicyConfig; + + beforeEach(() => { + policy = policyFactory(); + }); + + it('removes device_control fields from Windows OS configuration', () => { + const result = removeDeviceControl(policy); + + expect(result.windows).not.toHaveProperty('device_control'); + expect(result.windows.popup).not.toHaveProperty('device_control'); + }); + + it('removes device_control fields from Mac OS configuration', () => { + const result = removeDeviceControl(policy); + + expect(result.mac).not.toHaveProperty('device_control'); + expect(result.mac.popup).not.toHaveProperty('device_control'); + }); + + it('preserves all other Windows fields when removing device_control', () => { + const result = removeDeviceControl(policy); + + // Check that all other Windows fields are preserved + expect(result.windows.malware).toEqual(policy.windows.malware); + expect(result.windows.ransomware).toEqual(policy.windows.ransomware); + expect(result.windows.memory_protection).toEqual(policy.windows.memory_protection); + expect(result.windows.behavior_protection).toEqual(policy.windows.behavior_protection); + expect(result.windows.events).toEqual(policy.windows.events); + expect(result.windows.logging).toEqual(policy.windows.logging); + expect(result.windows.antivirus_registration).toEqual(policy.windows.antivirus_registration); + expect(result.windows.attack_surface_reduction).toEqual( + policy.windows.attack_surface_reduction + ); + + // Check that all other Windows popup fields are preserved + expect(result.windows.popup.malware).toEqual(policy.windows.popup.malware); + expect(result.windows.popup.ransomware).toEqual(policy.windows.popup.ransomware); + expect(result.windows.popup.memory_protection).toEqual( + policy.windows.popup.memory_protection + ); + expect(result.windows.popup.behavior_protection).toEqual( + policy.windows.popup.behavior_protection + ); + }); + + it('preserves all other Mac fields when removing device_control', () => { + const result = removeDeviceControl(policy); + + // Check that all other Mac fields are preserved + expect(result.mac.malware).toEqual(policy.mac.malware); + expect(result.mac.memory_protection).toEqual(policy.mac.memory_protection); + expect(result.mac.behavior_protection).toEqual(policy.mac.behavior_protection); + expect(result.mac.events).toEqual(policy.mac.events); + expect(result.mac.logging).toEqual(policy.mac.logging); + expect(result.mac.advanced).toEqual(policy.mac.advanced); + + // Check that all other Mac popup fields are preserved + expect(result.mac.popup.malware).toEqual(policy.mac.popup.malware); + expect(result.mac.popup.memory_protection).toEqual(policy.mac.popup.memory_protection); + expect(result.mac.popup.behavior_protection).toEqual(policy.mac.popup.behavior_protection); + }); + + it('preserves global and Linux configurations unchanged', () => { + const result = removeDeviceControl(policy); + + // Check that global fields are preserved + expect(result.global_manifest_version).toEqual(policy.global_manifest_version); + expect(result.global_telemetry_enabled).toEqual(policy.global_telemetry_enabled); + expect(result.meta).toEqual(policy.meta); + + // Check that Linux configuration is completely preserved (no device_control in Linux) + expect(result.linux).toEqual(policy.linux); + }); + + it('works correctly with custom device_control values', () => { + // Set custom device_control values + policy.windows.device_control = { enabled: true, usb_storage: 'block' }; + policy.mac.device_control = { enabled: true, usb_storage: 'audit' }; + policy.windows.popup.device_control = { enabled: true, message: 'Windows custom message' }; + policy.mac.popup.device_control = { enabled: false, message: 'Mac custom message' }; + + const result = removeDeviceControl(policy); + + // Verify device_control fields are completely removed + expect(result.windows).not.toHaveProperty('device_control'); + expect(result.mac).not.toHaveProperty('device_control'); + expect(result.windows.popup).not.toHaveProperty('device_control'); + expect(result.mac.popup).not.toHaveProperty('device_control'); + + // Verify other fields are still preserved + expect(result.windows.malware).toEqual(policy.windows.malware); + expect(result.mac.malware).toEqual(policy.mac.malware); + }); + + it('returns a new policy object without mutating the original', () => { + const originalPolicy = JSON.parse(JSON.stringify(policy)); // Deep clone for comparison + const result = removeDeviceControl(policy); + + // Verify original policy is unchanged + expect(policy).toEqual(originalPolicy); + expect(policy.windows.device_control).toBeDefined(); + expect(policy.mac.device_control).toBeDefined(); + expect(policy.windows.popup.device_control).toBeDefined(); + expect(policy.mac.popup.device_control).toBeDefined(); + + // Verify result is a different object + expect(result).not.toBe(policy); + expect(result.windows).not.toBe(policy.windows); + expect(result.mac).not.toBe(policy.mac); + }); + }); }); // This constant makes sure that if the type `PolicyConfig` is ever modified, @@ -360,11 +475,13 @@ const eventsOnlyPolicy = (): PolicyConfig => ({ ransomware: { mode: ProtectionModes.off, supported: true }, memory_protection: { mode: ProtectionModes.off, supported: true }, behavior_protection: { mode: ProtectionModes.off, supported: true, reputation_service: false }, + device_control: { enabled: false, usb_storage: 'audit' }, popup: { malware: { message: '', enabled: false }, ransomware: { message: '', enabled: false }, memory_protection: { message: '', enabled: false }, behavior_protection: { message: '', enabled: false }, + device_control: { message: '', enabled: false }, }, logging: { file: 'info' }, antivirus_registration: { enabled: false, mode: AntivirusRegistrationModes.disabled }, @@ -375,10 +492,12 @@ const eventsOnlyPolicy = (): PolicyConfig => ({ malware: { mode: ProtectionModes.off, blocklist: false, on_write_scan: false }, behavior_protection: { mode: ProtectionModes.off, supported: true, reputation_service: false }, memory_protection: { mode: ProtectionModes.off, supported: true }, + device_control: { enabled: false, usb_storage: 'audit' }, popup: { malware: { message: '', enabled: false }, behavior_protection: { message: '', enabled: false }, memory_protection: { message: '', enabled: false }, + device_control: { message: '', enabled: false }, }, logging: { file: 'info' }, advanced: { diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/models/policy_config_helpers.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/models/policy_config_helpers.ts index 5079493724d78..1ae22fe142bfa 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/models/policy_config_helpers.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/models/policy_config_helpers.ts @@ -9,7 +9,12 @@ import { get } from 'lodash'; import { set } from '@kbn/safer-lodash-set'; import { DefaultPolicyNotificationMessage } from './policy_config'; import type { PolicyConfig } from '../types'; -import { PolicyOperatingSystem, ProtectionModes, AntivirusRegistrationModes } from '../types'; +import { + PolicyOperatingSystem, + ProtectionModes, + AntivirusRegistrationModes, + DeviceControlAccessLevel, +} from '../types'; interface PolicyProtectionReference { keyPath: string; @@ -44,6 +49,10 @@ const getPolicyPopupReference = (): Array<{ keyPath: 'popup.ransomware.message', osList: [PolicyOperatingSystem.windows], }, + { + keyPath: 'popup.device_control.message', + osList: [PolicyOperatingSystem.windows, PolicyOperatingSystem.mac], + }, ]; export const getPolicyProtectionsReference = (): PolicyProtectionReference[] => [ @@ -102,6 +111,32 @@ export const disableProtections = (policy: PolicyConfig): PolicyConfig => { popup: { ...result.windows.popup, ...getDisabledWindowsSpecificPopups(result), + device_control: { + ...result.windows.popup.device_control, + enabled: false, + message: result.windows.popup.device_control?.message || '', + }, + }, + device_control: { + ...result.windows.device_control, + enabled: false, + usb_storage: DeviceControlAccessLevel.audit, + }, + }, + mac: { + ...result.mac, + device_control: { + ...result.mac.device_control, + enabled: false, + usb_storage: DeviceControlAccessLevel.audit, + }, + popup: { + ...result.mac.popup, + device_control: { + ...result.mac.popup.device_control, + enabled: false, + message: result.mac.popup.device_control?.message || '', + }, }, }, }; @@ -185,6 +220,10 @@ const getDisabledWindowsSpecificPopups = (policy: PolicyConfig) => ({ ...policy.windows.popup.ransomware, enabled: false, }, + device_control: { + ...policy.windows.popup.device_control, + enabled: false, + }, }); /** @@ -259,3 +298,34 @@ export const resetCustomNotifications = ( return acc; }, {}); }; + +/** + * Returns a copy of the passed `PolicyConfig` with device_control fields completely removed + * from both Windows and Mac OS configurations and their popup settings. + * + * @param policy + * @returns PolicyConfig without device_control fields + */ +export const removeDeviceControl = (policy: PolicyConfig): PolicyConfig => { + const { device_control: windowsDeviceControl, ...windowsRest } = policy.windows; + const { device_control: macDeviceControl, ...macRest } = policy.mac; + + const { device_control: windowsPopupDeviceControl, ...windowsPopupRest } = policy.windows.popup; + const { device_control: macPopupDeviceControl, ...macPopupRest } = policy.mac.popup; + + return { + ...policy, + windows: { + ...windowsRest, + popup: { + ...windowsPopupRest, + }, + }, + mac: { + ...macRest, + popup: { + ...macPopupRest, + }, + }, + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/index.ts index bffe6d8f3984c..628ba6113e6c8 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/index.ts @@ -999,6 +999,7 @@ export interface PolicyConfig { memory_protection: ProtectionFields & SupportedFields; behavior_protection: BehaviorProtectionFields & SupportedFields; ransomware: ProtectionFields & SupportedFields; + device_control?: DeviceControlFields; logging: { file: string; }; @@ -1019,6 +1020,10 @@ export interface PolicyConfig { message: string; enabled: boolean; }; + device_control?: { + message: string; + enabled: boolean; + }; }; antivirus_registration: { mode: AntivirusRegistrationModes; @@ -1042,6 +1047,7 @@ export interface PolicyConfig { malware: ProtectionFields & BlocklistFields & OnWriteScanFields; behavior_protection: BehaviorProtectionFields & SupportedFields; memory_protection: ProtectionFields & SupportedFields; + device_control?: DeviceControlFields; popup: { malware: { message: string; @@ -1055,6 +1061,10 @@ export interface PolicyConfig { message: string; enabled: boolean; }; + device_control?: { + message: string; + enabled: boolean; + }; }; logging: { file: string; @@ -1110,13 +1120,20 @@ export interface UIPolicyConfig { | 'memory_protection' | 'behavior_protection' | 'attack_surface_reduction' + | 'device_control' >; /** * Mac-specific policy configuration that is supported via the UI */ mac: Pick< PolicyConfig['mac'], - 'malware' | 'events' | 'popup' | 'advanced' | 'behavior_protection' | 'memory_protection' + | 'malware' + | 'events' + | 'popup' + | 'advanced' + | 'behavior_protection' + | 'memory_protection' + | 'device_control' >; /** * Linux-specific policy configuration that is supported via the UI @@ -1162,6 +1179,21 @@ export enum AntivirusRegistrationModes { sync = 'sync_with_malware_prevent', } +export const DeviceControlAccessLevel = { + audit: 'audit', // read and write + read_only: 'read_only', + execute_only: 'execute_only', + block: 'block', +} as const; + +export type DeviceControlAccessLevel = + (typeof DeviceControlAccessLevel)[keyof typeof DeviceControlAccessLevel]; + +export interface DeviceControlFields { + enabled: boolean; + usb_storage: DeviceControlAccessLevel; +} + /** * Endpoint Policy data, which extends Ingest's `PackagePolicy` type */ diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts index d96eb47bb741a..a940479f513e4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts @@ -300,6 +300,10 @@ describe('policy details: ', () => { supported: false, reputation_service: false, }, + device_control: { + enabled: false, + usb_storage: 'audit', + }, ransomware: { mode: 'off', supported: false }, attack_surface_reduction: { credential_hardening: { @@ -323,6 +327,10 @@ describe('policy details: ', () => { enabled: false, message: '', }, + device_control: { + enabled: false, + message: '', + }, }, logging: { file: 'info' }, antivirus_registration: { @@ -338,6 +346,10 @@ describe('policy details: ', () => { supported: false, reputation_service: false, }, + device_control: { + enabled: false, + usb_storage: 'audit', + }, memory_protection: { mode: 'off', supported: false }, popup: { malware: { @@ -352,6 +364,10 @@ describe('policy details: ', () => { enabled: false, message: '', }, + device_control: { + enabled: false, + message: '', + }, }, logging: { file: 'info' }, advanced: { diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts index bf87eab2a7293..07622d5bd70a3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts @@ -79,6 +79,14 @@ export const policySettingsMiddlewareRunner: MiddlewareRunner = async ( policyItem.inputs[0].config.policy.value.linux.popup.behavior_protection.message = DefaultPolicyRuleNotificationMessage; } + if (policyItem.inputs[0].config.policy.value.windows.popup.device_control?.message === '') { + policyItem.inputs[0].config.policy.value.windows.popup.device_control.message = + DefaultPolicyRuleNotificationMessage; + } + if (policyItem.inputs[0].config.policy.value.mac.popup.device_control?.message === '') { + policyItem.inputs[0].config.policy.value.mac.popup.device_control.message = + DefaultPolicyRuleNotificationMessage; + } } catch (error) { dispatch({ type: 'serverFailedToReturnPolicyDetailsData', diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts index 7e79fb9c57cdd..2cc4fed6517bb 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts @@ -166,6 +166,7 @@ export const policyConfig: (s: PolicyDetailsState) => UIPolicyConfig = createSel ransomware: windows.ransomware, memory_protection: windows.memory_protection, behavior_protection: windows.behavior_protection, + device_control: windows.device_control, popup: windows.popup, antivirus_registration: windows.antivirus_registration, attack_surface_reduction: windows.attack_surface_reduction, @@ -176,6 +177,7 @@ export const policyConfig: (s: PolicyDetailsState) => UIPolicyConfig = createSel malware: mac.malware, behavior_protection: mac.behavior_protection, memory_protection: mac.memory_protection, + device_control: mac.device_control, popup: mac.popup, }, linux: { diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/types.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/types.ts index 8ccf5f122baf9..9cf9d19df1c97 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/types.ts @@ -20,6 +20,7 @@ import type { PolicyData, UIPolicyConfig, MaybeImmutable, + DeviceControlFields, } from '../../../../common/endpoint/types'; import type { ServerApiError } from '../../../common/types'; import type { ImmutableMiddlewareAPI } from '../../../common/store'; @@ -130,6 +131,11 @@ export type BehaviorProtectionOSes = KeysByValueCriteria< { behavior_protection: ProtectionFields } >; +export type DeviceControlOSes = KeysByValueCriteria< + UIPolicyConfig, + { device_control?: DeviceControlFields } +>; + /** Returns an array of the policy OSes that have a ransomware protection field */ export type RansomwareProtectionOSes = KeysByValueCriteria< UIPolicyConfig, diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/device_control_card.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/device_control_card.tsx index 8a3ab10bc026e..2544205672213 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/device_control_card.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/device_control_card.tsx @@ -14,14 +14,16 @@ import { useTestIdGenerator } from '../../../../../../hooks/use_test_id_generato import { useLicense } from '../../../../../../../common/hooks/use_license'; import { SettingLockedCard } from '../setting_locked_card'; import { SettingCard } from '../setting_card'; -import { ProtectionSettingCardSwitch } from '../protection_setting_card_switch'; +import { DeviceControlSettingCardSwitch } from '../device_control_setting_card_switch'; +import { DeviceControlProtectionLevel } from '../device_control_protection_level'; +import { DeviceControlNotifyUserOption } from '../device_control_notify_user_option'; import { PolicyOperatingSystem, type Immutable, } from '../../../../../../../../common/endpoint/types'; -import type { RansomwareProtectionOSes } from '../../../../types'; +import type { DeviceControlOSes } from '../../../../types'; -export type UsbDeviceProtectionProps = PolicyFormComponentCommonProps; +export type DeviceControlProps = PolicyFormComponentCommonProps; export const DEVICE_CONTROL_CARD_TITLE = i18n.translate( 'xpack.securitySolution.endpoint.policy.details.deviceControl', @@ -30,36 +32,34 @@ export const DEVICE_CONTROL_CARD_TITLE = i18n.translate( } ); -const protectionLabel = i18n.translate( - 'xpack.securitySolution.endpoint.policy.protections.deviceControl', - { - defaultMessage: 'Device Control', - } -); - -const DEVICE_CONTROL_OS_VALUES: Immutable = [ +const DEVICE_CONTROL_OS_VALUES: Immutable = [ PolicyOperatingSystem.windows, - // PolicyOperatingSystem.mac, + PolicyOperatingSystem.mac, ]; /** * The Malware Protections form for policy details * which will configure for all relevant OSes. */ -export const DeviceControlCard = React.memo( +export const DeviceControlCard = React.memo( ({ policy, onChange, mode = 'edit', 'data-test-subj': dataTestSubj }) => { const getTestId = useTestIdGenerator(dataTestSubj); const isEnterprise = useLicense().isEnterprise(); - // const shouldRenderComponent = isProtectionsAllowed && isTrustedDevicesAllowed && isEnterprise; - // const selected = (policy && policy.windows[protection].mode) !== ProtectionModes.off; + // Check if device_control exists in policy (backward compatibility) + const deviceControlExists = + policy.windows.device_control !== undefined && policy.mac.device_control !== undefined; + const selected = Boolean( + deviceControlExists && + (policy.windows.device_control?.enabled || policy.mac.device_control?.enabled) + ); - // const protectionLabel = i18n.translate( - // 'xpack.securitySolution.endpoint.policy.protections.malware', - // { - // defaultMessage: 'Malware protections', - // } - // ); + const protectionLabel = i18n.translate( + 'xpack.securitySolution.endpoint.policy.protections.deviceControl', + { + defaultMessage: 'Device Control', + } + ); if (!isEnterprise) { return ( @@ -78,40 +78,34 @@ export const DeviceControlCard = React.memo( })} supportedOss={[OperatingSystem.WINDOWS, OperatingSystem.MAC]} dataTestSubj={getTestId()} - // selected={selected} - selected + selected={selected} mode={mode} rightCorner={ - } > - {/* - */} + /> diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/notify_user_option.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/notify_user_option.test.tsx index eb0686d7e07ef..2b3a5542ae662 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/notify_user_option.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/notify_user_option.test.tsx @@ -13,17 +13,17 @@ import React from 'react'; import { createLicenseServiceMock } from '../../../../../../../common/license/mocks'; import { licenseService as licenseServiceMocked } from '../../../../../../common/hooks/__mocks__/use_license'; import type { NotifyUserOptionProps } from './notify_user_option'; -import { - CUSTOMIZE_NOTIFICATION_MESSAGE_LABEL, - NOTIFY_USER_CHECKBOX_LABEL, - NOTIFY_USER_SECTION_TITLE, - NotifyUserOption, -} from './notify_user_option'; +import { NotifyUserOption } from './notify_user_option'; import { expectIsViewOnly, exactMatchText } from '../mocks'; import { cloneDeep } from 'lodash'; import { set } from '@kbn/safer-lodash-set'; import { ProtectionModes } from '../../../../../../../common/endpoint/types'; import userEvent from '@testing-library/user-event'; +import { + NOTIFY_USER_SECTION_TITLE, + NOTIFY_USER_CHECKBOX_LABEL, + CUSTOMIZE_NOTIFICATION_MESSAGE_LABEL, +} from './shared_translations'; jest.mock('../../../../../../common/hooks/use_license'); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/notify_user_option.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/notify_user_option.tsx index 62bfb9f665745..d305ba983d3f1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/notify_user_option.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/notify_user_option.tsx @@ -29,32 +29,12 @@ import type { ImmutableArray, UIPolicyConfig } from '../../../../../../../common import { ProtectionModes } from '../../../../../../../common/endpoint/types'; import type { PolicyProtection, MacPolicyProtection, LinuxPolicyProtection } from '../../../types'; import { useGetCustomNotificationUnavailableComponent } from '../hooks/use_get_custom_notification_unavailable_component'; - -export const NOTIFY_USER_SECTION_TITLE = i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.userNotification', - { defaultMessage: 'User notification' } -); - -export const NOTIFY_USER_CHECKBOX_LABEL = i18n.translate( - 'xpack.securitySolution.endpoint.policyDetail.notifyUser', - { - defaultMessage: 'Notify user', - } -); - -const NOTIFICATION_MESSAGE_LABEL = i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.notificationMessage', - { - defaultMessage: 'Notification message', - } -); - -export const CUSTOMIZE_NOTIFICATION_MESSAGE_LABEL = i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.customizeUserNotification', - { - defaultMessage: 'Customize notification message', - } -); +import { + NOTIFY_USER_SECTION_TITLE, + NOTIFY_USER_CHECKBOX_LABEL, + NOTIFICATION_MESSAGE_LABEL, + CUSTOMIZE_NOTIFICATION_MESSAGE_LABEL, +} from './shared_translations'; export interface NotifyUserOptionProps extends PolicyFormComponentCommonProps { protection: PolicyProtection; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/mocks.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/mocks.ts index f9c02ad2606ee..2ec3aa889abf2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/mocks.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/mocks.ts @@ -27,6 +27,7 @@ export const getPolicySettingsFormTestSubjects = ( const linuxEventsTestSubj = genTestSubj.withPrefix('linuxEvents'); const antivirusTestSubj = genTestSubj.withPrefix('antivirusRegistration'); const attackSurfaceTestSubj = genTestSubj.withPrefix('attackSurface'); + const deviceControlTestSubj = genTestSubj.withPrefix('deviceControl'); return { form: genTestSubj(), @@ -133,6 +134,15 @@ export const getPolicySettingsFormTestSubjects = ( syncRadioButton: antivirusTestSubj(AntivirusRegistrationModes.sync), osValueContainer: antivirusTestSubj('osValueContainer'), }, + deviceControl: { + card: deviceControlTestSubj(), + lockedCard: deviceControlTestSubj('locked'), + lockedCardTitle: deviceControlTestSubj('locked-title'), + enableDisableSwitch: deviceControlTestSubj('enableDisableSwitch'), + protectionAuditRadio: deviceControlTestSubj('protectionLevel-auditRadio'), + notifyUserCheckbox: deviceControlTestSubj('notifyUser-checkbox'), + osValuesContainer: deviceControlTestSubj('osValues'), + }, advancedSection: { container: advancedSectionTestSubj(''), showHideButton: advancedSectionTestSubj('showButton'), diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.tsx index dd5b75ebb82e0..2b33084754fbc 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.tsx @@ -63,7 +63,7 @@ export const PolicySettingsForm = memo((props) => { {DeviceControlUpSellingComponent ? ( ) : ( - + )} diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index 64fbf6b694eae..9984ee6d4b65a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -163,6 +163,7 @@ export class EndpointAppContextService { licenseService, telemetryConfigProvider, productFeaturesService, + experimentalFeatures, } = this.startDependencies; const logger = this.createLogger('endpointFleetExtension'); @@ -187,7 +188,8 @@ export class EndpointAppContextService { licenseService, this.setupDependencies.cloud, productFeaturesService, - telemetryConfigProvider + telemetryConfigProvider, + experimentalFeatures ) ); @@ -195,7 +197,12 @@ export class EndpointAppContextService { registerFleetCallback( 'packagePolicyUpdate', - getPackagePolicyUpdateCallback(this, this.setupDependencies.cloud, productFeaturesService) + getPackagePolicyUpdateCallback( + this, + this.setupDependencies.cloud, + productFeaturesService, + experimentalFeatures + ) ); registerFleetCallback('packagePolicyPostUpdate', getPackagePolicyPostUpdateCallback(this)); diff --git a/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts b/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts index b64a7193c2587..2b942efe2f23c 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts @@ -88,6 +88,7 @@ import { FleetPackagePolicyGenerator } from '../../common/endpoint/data_generato import { RESPONSE_ACTIONS_SUPPORTED_INTEGRATION_TYPES } from '../../common/endpoint/service/response_actions/constants'; import { pick } from 'lodash'; import { ENDPOINT_ACTIONS_INDEX } from '../../common/endpoint/constants'; +import type { ExperimentalFeatures } from '../../common'; jest.mock('uuid', () => ({ v4: (): string => 'NEW_UUID', @@ -127,6 +128,7 @@ describe('Fleet integrations', () => { let productFeaturesService: ProductFeaturesService; let endpointMetadataService: EndpointMetadataService; let logger: Logger; + let experimentalFeatures: ExperimentalFeatures; beforeEach(() => { endpointAppContextStartContract = createMockEndpointAppContextServiceStartContract(); @@ -135,6 +137,9 @@ describe('Fleet integrations', () => { licenseEmitter = new Subject(); licenseService = new LicenseService(); licenseService.start(licenseEmitter); + experimentalFeatures = { + trustedDevicesEnabled: true, + } as ExperimentalFeatures; productFeaturesService = endpointAppContextStartContract.productFeaturesService; const metadataMocks = createEndpointMetadataServiceTestContextMock(); @@ -197,7 +202,8 @@ describe('Fleet integrations', () => { licenseService, cloudService, productFeaturesService, - telemetryConfigProviderMock + telemetryConfigProviderMock, + experimentalFeatures ); return callback( @@ -662,7 +668,8 @@ describe('Fleet integrations', () => { const callback = getPackagePolicyUpdateCallback( endpointAppContextServiceMock, cloudService, - productFeaturesService + productFeaturesService, + experimentalFeatures ); const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value = mockPolicy; @@ -678,7 +685,8 @@ describe('Fleet integrations', () => { const callback = getPackagePolicyUpdateCallback( endpointAppContextServiceMock, cloudService, - productFeaturesService + productFeaturesService, + experimentalFeatures ); const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value = mockPolicy; @@ -708,7 +716,8 @@ describe('Fleet integrations', () => { const callback = getPackagePolicyUpdateCallback( endpointAppContextServiceMock, cloudService, - productFeaturesService + productFeaturesService, + experimentalFeatures ); const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value.global_manifest_version = '2023-01-01'; @@ -726,7 +735,8 @@ describe('Fleet integrations', () => { const callback = getPackagePolicyUpdateCallback( endpointAppContextServiceMock, cloudService, - productFeaturesService + productFeaturesService, + experimentalFeatures ); const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value.windows.popup.ransomware.message = 'foo'; @@ -778,7 +788,8 @@ describe('Fleet integrations', () => { const callback = getPackagePolicyUpdateCallback( endpointAppContextServiceMock, cloudService, - productFeaturesService + productFeaturesService, + experimentalFeatures ); const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value = { @@ -839,7 +850,8 @@ describe('Fleet integrations', () => { const callback = getPackagePolicyUpdateCallback( endpointAppContextServiceMock, cloudService, - productFeaturesService + productFeaturesService, + experimentalFeatures ); const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value = { @@ -878,7 +890,8 @@ describe('Fleet integrations', () => { const callback = getPackagePolicyUpdateCallback( endpointAppContextServiceMock, cloudService, - productFeaturesService + productFeaturesService, + experimentalFeatures ); const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value = mockPolicy; @@ -899,7 +912,8 @@ describe('Fleet integrations', () => { const callback = getPackagePolicyUpdateCallback( endpointAppContextServiceMock, cloudService, - productFeaturesService + productFeaturesService, + experimentalFeatures ); const updatedPolicy = await callback( @@ -974,7 +988,8 @@ describe('Fleet integrations', () => { const callback = getPackagePolicyUpdateCallback( endpointAppContextServiceMock, cloudService, - productFeaturesService + productFeaturesService, + experimentalFeatures ); const policyConfig = generator.generatePolicyPackagePolicy(); @@ -1008,7 +1023,8 @@ describe('Fleet integrations', () => { const callback = getPackagePolicyUpdateCallback( endpointAppContextServiceMock, cloudService, - productFeaturesService + productFeaturesService, + experimentalFeatures ); const policyConfig = generator.generatePolicyPackagePolicy(); // values should be updated @@ -1030,6 +1046,128 @@ describe('Fleet integrations', () => { }); }); + describe('when device control features are disabled', () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + beforeEach(() => { + licenseEmitter.next(Enterprise); + }); + + it('should remove device control when endpointTrustedDevices product feature is disabled', async () => { + productFeaturesService = createProductFeaturesServiceMock( + ALL_PRODUCT_FEATURE_KEYS.filter( + (key) => key !== ProductFeatureSecurityKey.endpointTrustedDevices + ) + ); + + const mockPolicy = policyFactory(); + // Add some device control settings to test removal + if (!mockPolicy.windows.device_control) { + mockPolicy.windows.device_control = { enabled: true, usb_storage: 'block' }; + } else { + mockPolicy.windows.device_control.enabled = true; + mockPolicy.windows.device_control.usb_storage = 'block'; + } + + const removeDeviceControlSpy = jest.spyOn(PolicyConfigHelpers, 'removeDeviceControl'); + + const callback = getPackagePolicyUpdateCallback( + endpointAppContextServiceMock, + cloudService, + productFeaturesService, + experimentalFeatures + ); + + const policyConfig = generator.generatePolicyPackagePolicy(); + policyConfig.inputs[0]!.config!.policy.value = mockPolicy; + + await callback( + policyConfig, + soClient, + esClient, + requestContextMock.convertContext(ctx), + req + ); + + expect(removeDeviceControlSpy).toHaveBeenCalledWith(mockPolicy); + }); + + it('should remove device control when trustedDevicesEnabled experimental feature is disabled', async () => { + // @ts-expect-error + experimentalFeatures.trustedDevicesEnabled = false; + + const mockPolicy = policyFactory(); + // Add some device control settings to test removal + if (!mockPolicy.windows.device_control) { + mockPolicy.windows.device_control = { enabled: true, usb_storage: 'block' }; + } else { + mockPolicy.windows.device_control.enabled = true; + mockPolicy.windows.device_control.usb_storage = 'block'; + } + + const removeDeviceControlSpy = jest.spyOn(PolicyConfigHelpers, 'removeDeviceControl'); + + const callback = getPackagePolicyUpdateCallback( + endpointAppContextServiceMock, + cloudService, + productFeaturesService, + experimentalFeatures + ); + + const policyConfig = generator.generatePolicyPackagePolicy(); + policyConfig.inputs[0]!.config!.policy.value = mockPolicy; + + await callback( + policyConfig, + soClient, + esClient, + requestContextMock.convertContext(ctx), + req + ); + + expect(removeDeviceControlSpy).toHaveBeenCalledWith(mockPolicy); + }); + + it('should not remove device control when both features are enabled', async () => { + // Reset to enabled states + // @ts-expect-error + experimentalFeatures.trustedDevicesEnabled = true; + // @ts-expect-error + productFeaturesService = createProductFeaturesServiceMock(ALL_PRODUCT_FEATURE_KEYS); + + const mockPolicy = policyFactory(); + if (!mockPolicy.windows.device_control) { + mockPolicy.windows.device_control = { enabled: true, usb_storage: 'block' }; + } else { + mockPolicy.windows.device_control.enabled = true; + mockPolicy.windows.device_control.usb_storage = 'block'; + } + + const removeDeviceControlSpy = jest.spyOn(PolicyConfigHelpers, 'removeDeviceControl'); + + const callback = getPackagePolicyUpdateCallback( + endpointAppContextServiceMock, + cloudService, + productFeaturesService, + experimentalFeatures + ); + + const policyConfig = generator.generatePolicyPackagePolicy(); + policyConfig.inputs[0]!.config!.policy.value = mockPolicy; + + await callback( + policyConfig, + soClient, + esClient, + requestContextMock.convertContext(ctx), + req + ); + + expect(removeDeviceControlSpy).not.toHaveBeenCalled(); + }); + }); + describe('when `antivirus_registration.mode` is changed', () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; @@ -1046,7 +1184,8 @@ describe('Fleet integrations', () => { callback = getPackagePolicyUpdateCallback( endpointAppContextServiceMock, cloudService, - productFeaturesService + productFeaturesService, + experimentalFeatures ); inputPolicyConfig = generator.generatePolicyPackagePolicy(); @@ -1141,7 +1280,8 @@ describe('Fleet integrations', () => { const callback = getPackagePolicyUpdateCallback( endpointAppContextServiceMock, cloudService, - productFeaturesService + productFeaturesService, + experimentalFeatures ); const policyConfig = generator.generatePolicyPackagePolicy(); diff --git a/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/fleet_integration.ts b/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/fleet_integration.ts index e6e8e2846f2e9..39b805372cc7d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/fleet_integration.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/fleet_integration.ts @@ -30,6 +30,7 @@ import type { PostAgentPolicyUpdateCallback, PutPackagePolicyPostUpdateCallback, } from '@kbn/fleet-plugin/server/types'; +import type { ExperimentalFeatures } from '../../common'; import { updateDeletedPolicyResponseActions } from './handlers/update_deleted_policy_response_actions'; import type { TelemetryConfigProvider } from '../../common/telemetry_config/telemetry_config_provider'; import type { EndpointInternalFleetServicesInterface } from '../endpoint/services/fleet'; @@ -42,6 +43,7 @@ import { isPolicySetToEventCollectionOnly, ensureOnlyEventCollectionIsAllowed, isBillablePolicy, + removeDeviceControl, } from '../../common/endpoint/models/policy_config_helpers'; import type { NewPolicyData, PolicyConfig, PolicyData } from '../../common/endpoint/types'; import type { LicenseService } from '../../common/license'; @@ -123,7 +125,8 @@ export const getPackagePolicyCreateCallback = ( licenseService: LicenseService, cloud: CloudSetup, productFeatures: ProductFeaturesService, - telemetryConfigProvider: TelemetryConfigProvider + telemetryConfigProvider: TelemetryConfigProvider, + experimentalFeatures: ExperimentalFeatures ): PostPackagePolicyCreateCallback => { return async ( newPackagePolicy, @@ -200,7 +203,8 @@ export const getPackagePolicyCreateCallback = ( cloud, esClientInfo, productFeatures, - telemetryConfigProvider + telemetryConfigProvider, + experimentalFeatures ); return { @@ -232,7 +236,8 @@ export const getPackagePolicyCreateCallback = ( export const getPackagePolicyUpdateCallback = ( endpointServices: EndpointAppContextService, cloud: CloudSetup, - productFeatures: ProductFeaturesService + productFeatures: ProductFeaturesService, + experimentalFeatures: ExperimentalFeatures ): PutPackagePolicyUpdateCallback => { const logger = endpointServices.createLogger('endpointPackagePolicyUpdateCallback'); const licenseService = endpointServices.getLicenseService(); @@ -319,6 +324,13 @@ export const getPackagePolicyUpdateCallback = ( endpointIntegrationData.inputs[0].config.policy.value = ensureOnlyEventCollectionIsAllowed(newEndpointPackagePolicy); } + if ( + !productFeatures.isEnabled(ProductFeatureSecurityKey.endpointTrustedDevices) || + !experimentalFeatures.trustedDevicesEnabled + ) { + endpointIntegrationData.inputs[0].config.policy.value = + removeDeviceControl(newEndpointPackagePolicy); + } updateAntivirusRegistrationEnabled(newEndpointPackagePolicy); diff --git a/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts b/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts index 3be27534e7e5d..84f5d59909959 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts @@ -24,6 +24,7 @@ import type { import type { ProductFeaturesService } from '../../lib/product_features_service/product_features_service'; import { createProductFeaturesServiceMock } from '../../lib/product_features_service/mocks'; import { createTelemetryConfigProviderMock } from '../../../common/telemetry_config/mocks'; +import type { ExperimentalFeatures } from '../../../common'; describe('Create Default Policy tests ', () => { const cloud = cloudMock.createSetup(); @@ -35,6 +36,9 @@ describe('Create Default Policy tests ', () => { let licenseService: LicenseService; let productFeaturesService: ProductFeaturesService; const telemetryConfigProviderMock = createTelemetryConfigProviderMock(); + const experimentalFeatures = { + trustedDevicesEnabled: true, + } as ExperimentalFeatures; const createDefaultPolicyCallback = async ( config?: AnyPolicyCreateConfig @@ -48,7 +52,8 @@ describe('Create Default Policy tests ', () => { cloud, esClientInfo, productFeaturesService, - telemetryConfigProviderMock + telemetryConfigProviderMock, + experimentalFeatures ); }; @@ -317,4 +322,93 @@ describe('Create Default Policy tests ', () => { expect(policyConfig.global_telemetry_enabled).toBe(false); }); }); + + describe('Device Control Removal', () => { + it('should remove device control when endpointTrustedDevices product feature is disabled', async () => { + const removeDeviceControlSpy = jest.spyOn(PolicyConfigHelpers, 'removeDeviceControl'); + productFeaturesService = createProductFeaturesServiceMock( + ALL_PRODUCT_FEATURE_KEYS.filter((key) => key !== 'endpoint_trusted_devices') + ); + + await createDefaultPolicyCallback(); + + expect(removeDeviceControlSpy).toHaveBeenCalledTimes(1); + removeDeviceControlSpy.mockRestore(); + }); + + it('should remove device control when trustedDevicesEnabled experimental feature is disabled', async () => { + const removeDeviceControlSpy = jest.spyOn(PolicyConfigHelpers, 'removeDeviceControl'); + const experimentalFeaturesWithTrustedDevicesDisabled = { + trustedDevicesEnabled: false, + } as ExperimentalFeatures; + + const createDefaultPolicyCallbackWithDisabledFeature = async ( + config?: AnyPolicyCreateConfig + ): Promise => { + const esClientInfo = await elasticsearchServiceMock + .createClusterClient() + .asInternalUser.info(); + esClientInfo.cluster_name = ''; + esClientInfo.cluster_uuid = ''; + return createDefaultPolicy( + licenseService, + config, + cloud, + esClientInfo, + productFeaturesService, + telemetryConfigProviderMock, + experimentalFeaturesWithTrustedDevicesDisabled + ); + }; + + await createDefaultPolicyCallbackWithDisabledFeature(); + + expect(removeDeviceControlSpy).toHaveBeenCalledTimes(1); + removeDeviceControlSpy.mockRestore(); + }); + + it('should remove device control when both endpointTrustedDevices product feature and trustedDevicesEnabled experimental feature are disabled', async () => { + const removeDeviceControlSpy = jest.spyOn(PolicyConfigHelpers, 'removeDeviceControl'); + productFeaturesService = createProductFeaturesServiceMock( + ALL_PRODUCT_FEATURE_KEYS.filter((key) => key !== 'endpoint_trusted_devices') + ); + const experimentalFeaturesWithTrustedDevicesDisabled = { + trustedDevicesEnabled: false, + } as ExperimentalFeatures; + + const createDefaultPolicyCallbackWithBothDisabled = async ( + config?: AnyPolicyCreateConfig + ): Promise => { + const esClientInfo = await elasticsearchServiceMock + .createClusterClient() + .asInternalUser.info(); + esClientInfo.cluster_name = ''; + esClientInfo.cluster_uuid = ''; + return createDefaultPolicy( + licenseService, + config, + cloud, + esClientInfo, + productFeaturesService, + telemetryConfigProviderMock, + experimentalFeaturesWithTrustedDevicesDisabled + ); + }; + + await createDefaultPolicyCallbackWithBothDisabled(); + + expect(removeDeviceControlSpy).toHaveBeenCalledTimes(1); + removeDeviceControlSpy.mockRestore(); + }); + + it('should NOT remove device control when both endpointTrustedDevices product feature and trustedDevicesEnabled experimental feature are enabled', async () => { + const removeDeviceControlSpy = jest.spyOn(PolicyConfigHelpers, 'removeDeviceControl'); + // Both features are enabled by default in the test setup + + await createDefaultPolicyCallback(); + + expect(removeDeviceControlSpy).not.toHaveBeenCalled(); + removeDeviceControlSpy.mockRestore(); + }); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts b/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts index 3f143ca27a845..48050961b1a33 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts @@ -8,6 +8,7 @@ import type { CloudSetup } from '@kbn/cloud-plugin/server'; import type { InfoResponse } from '@elastic/elasticsearch/lib/api/types'; import { ProductFeatureSecurityKey } from '@kbn/security-solution-features/keys'; +import type { ExperimentalFeatures } from '../../../common'; import type { TelemetryConfigProvider } from '../../../common/telemetry_config/telemetry_config_provider'; import { policyFactory as policyConfigFactory, @@ -26,6 +27,7 @@ import { disableProtections, ensureOnlyEventCollectionIsAllowed, isBillablePolicy, + removeDeviceControl, } from '../../../common/endpoint/models/policy_config_helpers'; import type { ProductFeaturesService } from '../../lib/product_features_service/product_features_service'; @@ -38,7 +40,8 @@ export const createDefaultPolicy = ( cloud: CloudSetup, esClientInfo: InfoResponse, productFeatures: ProductFeaturesService, - telemetryConfigProvider: TelemetryConfigProvider + telemetryConfigProvider: TelemetryConfigProvider, + experimentalFeatures: ExperimentalFeatures ): PolicyConfig => { // Pass license and cloud information to use in Policy creation const factoryPolicy = policyConfigFactory({ @@ -65,6 +68,13 @@ export const createDefaultPolicy = ( defaultPolicyPerType = ensureOnlyEventCollectionIsAllowed(defaultPolicyPerType); } + if ( + !productFeatures.isEnabled(ProductFeatureSecurityKey.endpointTrustedDevices) || + !experimentalFeatures.trustedDevicesEnabled + ) { + defaultPolicyPerType = removeDeviceControl(defaultPolicyPerType); + } + defaultPolicyPerType.meta.billable = isBillablePolicy(defaultPolicyPerType); return defaultPolicyPerType; From a4a8dea66eb94b2bd6bcc4aa5acf6c10a8648553 Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Fri, 25 Jul 2025 14:58:41 +0200 Subject: [PATCH 07/26] chore: remove legacy locked card upgrade message from translations --- .../plugins/private/translations/translations/fr-FR.json | 1 - .../plugins/private/translations/translations/ja-JP.json | 1 - .../plugins/private/translations/translations/zh-CN.json | 1 - 3 files changed, 3 deletions(-) diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index 3ae0dbe0793df..02cd6c02cf848 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -37362,7 +37362,6 @@ "xpack.securitySolution.endpoint.policy.details.detectionRulesMessageDocsLink": "En savoir plus", "xpack.securitySolution.endpoint.policy.details.eventCollection": "Collection d'événements", "xpack.securitySolution.endpoint.policy.details.eventCollectionsEnabled": "{selected} collection(s) d'événements activée(s) sur {total}", - "xpack.securitySolution.endpoint.policy.details.lockedCardUpgradeMessage": "Pour activer cette protection, vous devez mettre à niveau votre licence vers Platinum, démarrer un essai gratuit de 30 jours ou lancer un {cloudDeploymentLink} sur AWS, GCP ou Azure.", "xpack.securitySolution.endpoint.policy.details.malware": "Malware", "xpack.securitySolution.endpoint.policy.details.memory": "Menace sur la mémoire", "xpack.securitySolution.endpoint.policy.details.memory_protection": "Menace sur la mémoire", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index 23f2a7330b300..a99eed14c0b57 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -37403,7 +37403,6 @@ "xpack.securitySolution.endpoint.policy.details.detectionRulesMessageDocsLink": "詳細情報", "xpack.securitySolution.endpoint.policy.details.eventCollection": "イベント収集", "xpack.securitySolution.endpoint.policy.details.eventCollectionsEnabled": "{selected} / {total} 件のイベント収集が有効です", - "xpack.securitySolution.endpoint.policy.details.lockedCardUpgradeMessage": "この保護をオンにするには、ライセンスをプラチナに更新するか、30日間の無料トライアルを開始するか、AWS、GCP、またはAzureで{cloudDeploymentLink}にサインアップしてください。", "xpack.securitySolution.endpoint.policy.details.malware": "マルウェア", "xpack.securitySolution.endpoint.policy.details.memory": "メモリ脅威", "xpack.securitySolution.endpoint.policy.details.memory_protection": "メモリ脅威", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index 40f1db92aff48..d8263fd6a34c0 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -37387,7 +37387,6 @@ "xpack.securitySolution.endpoint.policy.details.detectionRulesMessageDocsLink": "了解详情", "xpack.securitySolution.endpoint.policy.details.eventCollection": "事件收集", "xpack.securitySolution.endpoint.policy.details.eventCollectionsEnabled": "{selected} / {total} 个事件收集已启用", - "xpack.securitySolution.endpoint.policy.details.lockedCardUpgradeMessage": "要打开此防护,必须将您的许可证升级到白金级、开始 30 天免费试用或在 AWS、GCP 或 Azure 中实施{cloudDeploymentLink}。", "xpack.securitySolution.endpoint.policy.details.malware": "恶意软件", "xpack.securitySolution.endpoint.policy.details.memory": "内存威胁", "xpack.securitySolution.endpoint.policy.details.memory_protection": "内存威胁", From 35cf852d2fa4feabcc86cd421f759e947c07340a Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Fri, 25 Jul 2025 16:26:04 +0200 Subject: [PATCH 08/26] feat: add device control license validation to endpoint policy config --- .../common/license/policy_config.test.ts | 189 ++++++++++++++++++ .../common/license/policy_config.ts | 44 ++++ 2 files changed, 233 insertions(+) diff --git a/x-pack/solutions/security/plugins/security_solution/common/license/policy_config.test.ts b/x-pack/solutions/security/plugins/security_solution/common/license/policy_config.test.ts index f32b8396d2e37..1ab405d8b6bb9 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/license/policy_config.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/license/policy_config.test.ts @@ -17,6 +17,7 @@ import { policyFactoryWithSupportedFeatures, } from '../endpoint/models/policy_config'; import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock'; +import type { PolicyConfig } from '../endpoint/types'; import { ProtectionModes } from '../endpoint/types'; describe('policy_config and licenses', () => { @@ -25,9 +26,17 @@ describe('policy_config and licenses', () => { const Gold = licenseMock.createLicense({ license: { type: 'gold', mode: 'gold' } }); const Basic = licenseMock.createLicense({ license: { type: 'basic', mode: 'basic' } }); + const disableEnterpriseFeatures = (policy: PolicyConfig) => { + if (policy.windows.device_control) policy.windows.device_control.enabled = false; + if (policy.mac.device_control) policy.mac.device_control.enabled = false; + if (policy.windows.popup.device_control) policy.windows.popup.device_control.enabled = false; + if (policy.mac.popup.device_control) policy.mac.popup.device_control.enabled = false; + }; + describe('isEndpointPolicyValidForLicense', () => { it('allows malware notification to be disabled with a Platinum license', () => { const policy = policyFactory(); + disableEnterpriseFeatures(policy); policy.windows.popup.malware.enabled = false; // make policy change const valid = isEndpointPolicyValidForLicense(policy, Platinum); expect(valid).toBeTruthy(); @@ -54,6 +63,7 @@ describe('policy_config and licenses', () => { it('allows malware notification message changes with a Platinum license', () => { const policy = policyFactory(); + disableEnterpriseFeatures(policy); policy.windows.popup.malware.message = 'BOOM'; // make policy change const valid = isEndpointPolicyValidForLicense(policy, Platinum); expect(valid).toBeTruthy(); @@ -126,6 +136,7 @@ describe('policy_config and licenses', () => { it('allows advanced rollback option when Platinum', () => { const policy = policyFactory(); + disableEnterpriseFeatures(policy); policy.windows.advanced = { alerts: { rollback: { self_healing: { enabled: true } } } }; // make policy change const valid = isEndpointPolicyValidForLicense(policy, Platinum); expect(valid).toBeTruthy(); @@ -143,6 +154,7 @@ describe('policy_config and licenses', () => { it('allows credential hardening option when Platinum', () => { const policy = policyFactory(); + disableEnterpriseFeatures(policy); policy.windows.attack_surface_reduction.credential_hardening.enabled = true; // make policy change const valid = isEndpointPolicyValidForLicense(policy, Platinum); expect(valid).toBeTruthy(); @@ -197,6 +209,7 @@ describe('policy_config and licenses', () => { it('allows ransomware notification message changes with a Platinum license', () => { const policy = policyFactory(); + disableEnterpriseFeatures(policy); policy.windows.popup.ransomware.message = 'BOOM'; const valid = isEndpointPolicyValidForLicense(policy, Platinum); expect(valid).toBeTruthy(); @@ -240,6 +253,7 @@ describe('policy_config and licenses', () => { it('allows memory_protection notification message changes with a Platinum license', () => { const policy = policyFactory(); + disableEnterpriseFeatures(policy); policy.windows.popup.memory_protection.message = 'BOOM'; policy.mac.popup.memory_protection.message = 'BOOM'; policy.linux.popup.memory_protection.message = 'BOOM'; @@ -287,6 +301,7 @@ describe('policy_config and licenses', () => { it('allows behavior_protection notification message changes with a Platinum license', () => { const policy = policyFactory(); + disableEnterpriseFeatures(policy); policy.windows.popup.behavior_protection.message = 'BOOM'; policy.mac.popup.behavior_protection.message = 'BOOM'; policy.linux.popup.behavior_protection.message = 'BOOM'; @@ -677,4 +692,178 @@ describe('policy_config and licenses', () => { expect(retPolicy.windows.events.file).toBeFalsy(); }); }); + + describe('isEndpointDeviceControlPolicyValidForLicense', () => { + it('allows any device control configuration with Enterprise license', () => { + const policy = policyFactory(); + // Enable all device control features + if (policy.windows.device_control) { + policy.windows.device_control.enabled = true; + } + if (policy.mac.device_control) { + policy.mac.device_control.enabled = true; + } + if (policy.windows.popup.device_control) { + policy.windows.popup.device_control.enabled = true; + policy.windows.popup.device_control.message = 'Custom message'; + } + if (policy.mac.popup.device_control) { + policy.mac.popup.device_control.enabled = true; + policy.mac.popup.device_control.message = 'Custom message'; + } + + const valid = isEndpointPolicyValidForLicense(policy, Enterprise); + expect(valid).toBeTruthy(); + }); + + it('blocks Windows device control when enabled with non-Enterprise license', () => { + const policy = policyFactory(); + if (policy.windows.device_control) { + policy.windows.device_control.enabled = true; + } + + let valid = isEndpointPolicyValidForLicense(policy, Platinum); + expect(valid).toBeFalsy(); + + valid = isEndpointPolicyValidForLicense(policy, Gold); + expect(valid).toBeFalsy(); + + valid = isEndpointPolicyValidForLicense(policy, Basic); + expect(valid).toBeFalsy(); + }); + + it('blocks Mac device control when enabled with non-Enterprise license', () => { + const policy = policyFactory(); + if (policy.mac.device_control) { + policy.mac.device_control.enabled = true; + } + + let valid = isEndpointPolicyValidForLicense(policy, Platinum); + expect(valid).toBeFalsy(); + + valid = isEndpointPolicyValidForLicense(policy, Gold); + expect(valid).toBeFalsy(); + + valid = isEndpointPolicyValidForLicense(policy, Basic); + expect(valid).toBeFalsy(); + }); + + it('blocks Windows popup device control when enabled with non-Enterprise license', () => { + const policy = policyFactory(); + if (policy.windows.popup.device_control) { + policy.windows.popup.device_control.enabled = true; + } + + let valid = isEndpointPolicyValidForLicense(policy, Platinum); + expect(valid).toBeFalsy(); + + valid = isEndpointPolicyValidForLicense(policy, Gold); + expect(valid).toBeFalsy(); + + valid = isEndpointPolicyValidForLicense(policy, Basic); + expect(valid).toBeFalsy(); + }); + + it('blocks Mac popup device control when enabled with non-Enterprise license', () => { + const policy = policyFactory(); + if (policy.mac.popup.device_control) { + policy.mac.popup.device_control.enabled = true; + } + + let valid = isEndpointPolicyValidForLicense(policy, Platinum); + expect(valid).toBeFalsy(); + + valid = isEndpointPolicyValidForLicense(policy, Gold); + expect(valid).toBeFalsy(); + + valid = isEndpointPolicyValidForLicense(policy, Basic); + expect(valid).toBeFalsy(); + }); + + it('blocks Windows popup device control custom message with non-Enterprise license', () => { + const policy = policyFactory(); + if (policy.windows.popup.device_control) { + policy.windows.popup.device_control.enabled = false; + policy.windows.popup.device_control.message = 'Custom message'; + } + + let valid = isEndpointPolicyValidForLicense(policy, Platinum); + expect(valid).toBeFalsy(); + + valid = isEndpointPolicyValidForLicense(policy, Gold); + expect(valid).toBeFalsy(); + + valid = isEndpointPolicyValidForLicense(policy, Basic); + expect(valid).toBeFalsy(); + }); + + it('blocks Mac popup device control custom message with non-Enterprise license', () => { + const policy = policyFactory(); + if (policy.mac.popup.device_control) { + policy.mac.popup.device_control.enabled = false; + policy.mac.popup.device_control.message = 'Custom message'; + } + + let valid = isEndpointPolicyValidForLicense(policy, Platinum); + expect(valid).toBeFalsy(); + + valid = isEndpointPolicyValidForLicense(policy, Gold); + expect(valid).toBeFalsy(); + + valid = isEndpointPolicyValidForLicense(policy, Basic); + expect(valid).toBeFalsy(); + }); + + it('allows Mac and Windows popup device control with empty message and disabled state for non-Enterprise license', () => { + const policy = policyFactory(); + if (policy.windows.device_control) { + policy.windows.device_control.enabled = false; + } + if (policy.windows.popup.device_control) { + policy.windows.popup.device_control.enabled = false; + policy.windows.popup.device_control.message = ''; + } + + if (policy.mac.device_control) { + policy.mac.device_control.enabled = false; + } + if (policy.mac.popup.device_control) { + policy.mac.popup.device_control.enabled = false; + policy.mac.popup.device_control.message = ''; + } + + const valid = isEndpointPolicyValidForLicense(policy, Platinum); + expect(valid).toBeTruthy(); + }); + + it('allows Mac and Windows popup device control with default message and disabled state for non-Enterprise license', () => { + const policy = policyFactory(); + + if (policy.windows.device_control) policy.windows.device_control.enabled = false; + if (policy.windows.popup.device_control) { + policy.windows.popup.device_control.enabled = false; + policy.windows.popup.device_control.message = DefaultPolicyRuleNotificationMessage; + } + if (policy.mac.device_control) { + policy.mac.device_control.enabled = false; + } + if (policy.mac.popup.device_control) { + policy.mac.popup.device_control.enabled = false; + policy.mac.popup.device_control.message = DefaultPolicyRuleNotificationMessage; + } + + const valid = isEndpointPolicyValidForLicense(policy, Platinum); + expect(valid).toBeTruthy(); + }); + + it('blocks device control with null license when features are enabled', () => { + const policy = policyFactory(); + if (policy.windows.device_control) { + policy.windows.device_control.enabled = true; + } + + const valid = isEndpointPolicyValidForLicense(policy, null); + expect(valid).toBeFalsy(); + }); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/common/license/policy_config.ts b/x-pack/solutions/security/plugins/security_solution/common/license/policy_config.ts index 214818c109ed3..d998fbd3bb2a2 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/license/policy_config.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/license/policy_config.ts @@ -244,6 +244,49 @@ function isEndpointAdvancedPolicyValidForLicense(policy: PolicyConfig, license: return true; } +function isEndpointDeviceControlPolicyValidForLicense( + policy: PolicyConfig, + license: ILicense | null +) { + if (isAtLeast(license, 'enterprise')) { + return true; + } + + if (policy.windows.device_control && policy.windows.device_control.enabled === true) { + return false; + } + + if (policy.windows.popup.device_control) { + if (policy.windows.popup.device_control.enabled === true) { + return false; + } + if ( + policy.windows.popup.device_control.message !== '' && + policy.windows.popup.device_control.message !== DefaultPolicyRuleNotificationMessage + ) { + return false; + } + } + + if (policy.mac.device_control && policy.mac.device_control.enabled === true) { + return false; + } + + if (policy.mac.popup.device_control) { + if (policy.mac.popup.device_control.enabled === true) { + return false; + } + if ( + policy.mac.popup.device_control.message !== '' && + policy.mac.popup.device_control.message !== DefaultPolicyRuleNotificationMessage + ) { + return false; + } + } + + return true; +} + function isEndpointProtectionUpdatesValidForLicense( policy: PolicyConfig, license: ILicense | null @@ -272,6 +315,7 @@ export const isEndpointPolicyValidForLicense = ( isEndpointBehaviorPolicyValidForLicense(policy, license) && isEndpointAdvancedPolicyValidForLicense(policy, license) && isEndpointCredentialDumpingPolicyValidForLicense(policy, license) && + isEndpointDeviceControlPolicyValidForLicense(policy, license) && isEndpointProtectionUpdatesValidForLicense(policy, license) ); }; From d0118aacdee87bedc8777b2b4f2444611c3d658c Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Sat, 26 Jul 2025 00:47:47 +0200 Subject: [PATCH 09/26] feat: add device control popup settings and update license checks to enterprise tier --- .../common/endpoint/models/policy_config.ts | 14 ++++++++++++++ .../fleet_integration/fleet_integration.test.ts | 13 ++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/models/policy_config.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/models/policy_config.ts index 1132034122ad4..83d5c6e756a2e 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/models/policy_config.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/models/policy_config.ts @@ -217,6 +217,13 @@ export const policyFactoryWithoutPaidEnterpriseFeatures = ( enabled: false, usb_storage: DeviceControlAccessLevel.audit, }, + popup: { + ...policy.windows.popup, + device_control: { + enabled: false, + message: '', + }, + }, }, mac: { ...policy.mac, @@ -224,6 +231,13 @@ export const policyFactoryWithoutPaidEnterpriseFeatures = ( enabled: false, usb_storage: DeviceControlAccessLevel.audit, }, + popup: { + ...policy.mac.popup, + device_control: { + enabled: false, + message: '', + }, + }, }, }; }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts b/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts index 2b942efe2f23c..77c6683166fbb 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts @@ -21,6 +21,7 @@ import { cloudMock } from '@kbn/cloud-plugin/server/mocks'; import { policyFactory, policyFactoryWithoutPaidFeatures, + policyFactoryWithoutPaidEnterpriseFeatures, } from '../../common/endpoint/models/policy_config'; import { buildManifestManagerMock } from '../endpoint/services/artifacts/manifest_manager/manifest_manager.mock'; import { @@ -846,7 +847,7 @@ describe('Fleet integrations', () => { ])( 'should return bad request for invalid endpoint package policy global manifest values', async ({ date, message }) => { - const mockPolicy = policyFactory(); // defaults with paid features on + const mockPolicy = policyFactoryWithoutPaidEnterpriseFeatures(); // Use platinum-compatible policy const callback = getPackagePolicyUpdateCallback( endpointAppContextServiceMock, cloudService, @@ -885,6 +886,7 @@ describe('Fleet integrations', () => { ); it('updates successfully when paid features are turned on', async () => { + licenseEmitter.next(Enterprise); // Temporarily use Enterprise for this test const mockPolicy = policyFactory(); mockPolicy.windows.popup.malware.message = 'paid feature'; const callback = getPackagePolicyUpdateCallback( @@ -906,6 +908,7 @@ describe('Fleet integrations', () => { }); it('should turn off protections if endpointPolicyProtections productFeature is disabled', async () => { + licenseEmitter.next(Enterprise); // Temporarily use Enterprise for this test productFeaturesService = createProductFeaturesServiceMock( ALL_PRODUCT_FEATURE_KEYS.filter((key) => key !== 'endpoint_policy_protections') ); @@ -973,13 +976,13 @@ describe('Fleet integrations', () => { esClient.info.mockResolvedValue(infoResponse); beforeEach(() => { - licenseEmitter.next(Platinum); // set license level to platinum + licenseEmitter.next(Enterprise); // set license level to enterprise }); it('updates successfully when meta fields differ from services', async () => { const mockPolicy = policyFactory(); mockPolicy.meta.cloud = true; // cloud mock will return true - mockPolicy.meta.license = 'platinum'; // license is set to emit platinum + mockPolicy.meta.license = 'enterprise'; // license is set to emit enterprise mockPolicy.meta.cluster_name = 'updated-name'; mockPolicy.meta.cluster_uuid = 'updated-uuid'; mockPolicy.meta.license_uuid = 'updated-uid'; @@ -1014,7 +1017,7 @@ describe('Fleet integrations', () => { it('meta fields stay the same where there is no difference', async () => { const mockPolicy = policyFactory(); mockPolicy.meta.cloud = true; // cloud mock will return true - mockPolicy.meta.license = 'platinum'; // license is set to emit platinum + mockPolicy.meta.license = 'enterprise'; // license is set to emit enterprise mockPolicy.meta.cluster_name = 'updated-name'; mockPolicy.meta.cluster_uuid = 'updated-uuid'; mockPolicy.meta.license_uuid = 'updated-uid'; @@ -1179,7 +1182,7 @@ describe('Fleet integrations', () => { config.inputs[0].config!.policy.value.windows.antivirus_registration.enabled; beforeEach(() => { - licenseEmitter.next(Platinum); + licenseEmitter.next(Enterprise); callback = getPackagePolicyUpdateCallback( endpointAppContextServiceMock, From 7d0f5057d895b53d5add4b911a8cbed2fcdcf0d6 Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Wed, 30 Jul 2025 10:08:43 +0200 Subject: [PATCH 10/26] feat: add trusted devices management functionality --- .../shared/deeplinks/security/deep_links.ts | 1 + .../shared/kbn-doc-links/src/get_doc_links.ts | 1 + .../shared/kbn-doc-links/src/types.ts | 1 + .../shared/fleet/common/constants/authz.ts | 12 ++ .../features/src/product_features_keys.ts | 6 + .../src/security/product_feature_config.ts | 4 + .../v3_features/kibana_sub_features.ts | 72 ++++++++ .../src/common/exception_list/index.ts | 2 + .../create_exception_list_schema/index.ts | 1 + .../index.ts | 5 + .../navigation_tree/assets_navigation_tree.ts | 4 + .../create_endpoint_trusted_devices_list.ts | 79 +++++++++ .../exception_lists/exception_list_client.ts | 16 ++ .../security_solution/common/constants.ts | 1 + .../endpoint/service/authz/authz.test.ts | 10 +- .../common/endpoint/service/authz/authz.ts | 6 + .../common/endpoint/types/authz.ts | 4 + .../common/experimental_features.ts | 6 + .../public/app/translations.ts | 6 + .../public/management/common/breadcrumbs.ts | 9 +- .../public/management/common/constants.ts | 2 + .../public/management/common/routing.ts | 24 +++ .../public/management/common/translations.ts | 4 + .../public/management/links.ts | 23 +++ .../public/management/pages/index.tsx | 19 ++ .../public/management/pages/policy/index.tsx | 5 + .../selectors/policy_common_selectors.ts | 14 ++ .../selectors/policy_settings_selectors.ts | 4 + .../endpoint_package_custom_extension.tsx | 49 +++++- .../translations.tsx | 17 ++ .../endpoint_policy_artifact_cards.tsx | 51 ++++++ .../translations.tsx | 23 +++ .../pages/policy/view/policy_hooks.ts | 7 + .../pages/policy/view/tabs/policy_tabs.tsx | 70 +++++++- .../view/tabs/trusted_devices_translations.ts | 166 ++++++++++++++++++ .../pages/trusted_apps/constants.ts | 12 +- .../pages/trusted_devices/constants.ts | 26 +++ .../pages/trusted_devices/index.tsx | 23 +++ .../trusted_devices/service/api_client.ts | 64 +++++++ .../view/components/artifacts_docs_link.tsx | 32 ++++ .../trusted_devices/view/components/form.tsx | 18 ++ .../trusted_devices/view/translations.ts | 88 ++++++++++ .../view/trusted_devices_list.tsx | 162 +++++++++++++++++ .../public/management/types.ts | 1 + .../common/pli/pli_config.ts | 1 + .../screens/serverless_security_header.ts | 2 + 46 files changed, 1137 insertions(+), 16 deletions(-) create mode 100644 x-pack/solutions/security/plugins/lists/server/services/exception_lists/create_endpoint_trusted_devices_list.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/trusted_devices_translations.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/constants.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/index.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/service/api_client.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/components/artifacts_docs_link.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/components/form.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/translations.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/trusted_devices_list.tsx diff --git a/src/platform/packages/shared/deeplinks/security/deep_links.ts b/src/platform/packages/shared/deeplinks/security/deep_links.ts index b5e7b1b3030f0..e8118fbece8dc 100644 --- a/src/platform/packages/shared/deeplinks/security/deep_links.ts +++ b/src/platform/packages/shared/deeplinks/security/deep_links.ts @@ -72,6 +72,7 @@ export enum SecurityPageName { timelines = 'timelines', timelinesTemplates = 'timelines-templates', trustedApps = 'trusted_apps', + trustedDevices = 'trusted_devices', users = 'users', usersAll = 'users-all', usersAnomalies = 'users-anomalies', diff --git a/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts b/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts index 8d4b360e45469..249c35fbc6885 100644 --- a/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts +++ b/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts @@ -445,6 +445,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D avcResults: `https://www.elastic.co/blog/elastic-security-av-comparatives-business-test`, bidirectionalIntegrations: `${ELASTIC_DOCS}solutions/security/endpoint-response-actions/third-party-response-actions`, trustedApps: `${ELASTIC_DOCS}solutions/security/manage-elastic-defend/trusted-applications`, + trustedDevices: `${ELASTIC_DOCS}solutions/security/manage-elastic-defend/trusted-applications`, // TODO: Update this link when trusted devices is available elasticAiFeatures: `${ELASTIC_DOCS}solutions/security/ai`, eventFilters: `${ELASTIC_DOCS}solutions/security/manage-elastic-defend/event-filters`, blocklist: `${ELASTIC_DOCS}solutions/security/manage-elastic-defend/blocklist`, diff --git a/src/platform/packages/shared/kbn-doc-links/src/types.ts b/src/platform/packages/shared/kbn-doc-links/src/types.ts index 1db1f8b90dde9..01c9ddc06246d 100644 --- a/src/platform/packages/shared/kbn-doc-links/src/types.ts +++ b/src/platform/packages/shared/kbn-doc-links/src/types.ts @@ -307,6 +307,7 @@ export interface DocLinks { readonly bidirectionalIntegrations: string; readonly thirdPartyLlmProviders: string; readonly trustedApps: string; + readonly trustedDevices: string; readonly elasticAiFeatures: string; readonly eventFilters: string; readonly eventMerging: string; diff --git a/x-pack/platform/plugins/shared/fleet/common/constants/authz.ts b/x-pack/platform/plugins/shared/fleet/common/constants/authz.ts index 290b86bf8edee..58a5bcb2307d7 100644 --- a/x-pack/platform/plugins/shared/fleet/common/constants/authz.ts +++ b/x-pack/platform/plugins/shared/fleet/common/constants/authz.ts @@ -60,6 +60,18 @@ export const ENDPOINT_PRIVILEGES: Record = deepFreez privilegeType: 'api', privilegeName: 'readTrustedApplications', }, + writeTrustedDevices: { + appId: DEFAULT_APP_CATEGORIES.security.id, + privilegeSplit: '-', + privilegeType: 'api', + privilegeName: 'writeTrustedDevices', + }, + readTrustedDevices: { + appId: DEFAULT_APP_CATEGORIES.security.id, + privilegeSplit: '-', + privilegeType: 'api', + privilegeName: 'readTrustedDevices', + }, writeHostIsolationExceptions: { appId: DEFAULT_APP_CATEGORIES.security.id, privilegeSplit: '-', 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..42626660c04d1 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 @@ -30,6 +30,11 @@ export enum ProductFeatureSecurityKey { * running endpoint security */ endpointHostManagement = 'endpoint_host_management', + + /** + * Enables access to the Trusted Devices + */ + endpointTrustedDevices = 'endpoint_trusted_devices', /** * Enables access to Endpoint host isolation and release actions */ @@ -179,6 +184,7 @@ export enum SecuritySubFeatureId { endpointList = 'endpointListSubFeature', endpointExceptions = 'endpointExceptionsSubFeature', trustedApplications = 'trustedApplicationsSubFeature', + trustedDevices = 'trustedDevicesSubFeature', hostIsolationExceptionsBasic = 'hostIsolationExceptionsBasicSubFeature', blocklist = 'blocklistSubFeature', eventFilters = 'eventFiltersSubFeature', 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..651fb05b5902b 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 @@ -119,6 +119,10 @@ export const securityDefaultProductFeaturesConfig: DefaultSecurityProductFeature subFeatureIds: [SecuritySubFeatureId.endpointList], }, + [ProductFeatureSecurityKey.endpointTrustedDevices]: { + subFeatureIds: [SecuritySubFeatureId.trustedDevices], + }, + [ProductFeatureSecurityKey.endpointPolicyManagement]: { subFeatureIds: [SecuritySubFeatureId.policyManagement], }, 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..f4202801e306b 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 @@ -137,6 +137,65 @@ const trustedApplicationsSubFeature = (): SubFeatureConfig => ({ }, ], }); + +const trustedDevicesSubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedDevices.privilegesTooltip', + { + defaultMessage: 'All Spaces is required for Trusted Devices access.', + } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedDevices', + { + defaultMessage: 'Trusted Devices', + } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedDevices.description', + { + defaultMessage: + 'Allows management of trusted USB and external devices that bypass device control protections.', + } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + api: [ + 'lists-all', + 'lists-read', + 'lists-summary', + `${APP_ID}-writeTrustedDevices`, + `${APP_ID}-readTrustedDevices`, + ], + id: 'trusted_devices_all', + includeIn: 'none', + name: TRANSLATIONS.all, + savedObject: { + all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC], + read: [], + }, + ui: ['writeTrustedDevices', 'readTrustedDevices'], + }, + { + api: ['lists-read', 'lists-summary', `${APP_ID}-readTrustedDevices`], + id: 'trusted_devices_read', + includeIn: 'none', + name: TRANSLATIONS.read, + savedObject: { + all: [], + read: [], + }, + ui: ['readTrustedDevices'], + }, + ], + }, + ], +}); + const hostIsolationExceptionsBasicSubFeature = (): SubFeatureConfig => ({ requireAllSpaces: true, privilegesTooltip: i18n.translate( @@ -852,6 +911,19 @@ export const getSecurityV3SubFeaturesMap = ({ ]); } + if (experimentalFeatures.trustedDevicesEnabled) { + // place between trusted applications and host isolation exceptions + const trustedAppsIndex = securitySubFeaturesList.findIndex( + ([id]) => id === SecuritySubFeatureId.trustedApplications + ); + if (trustedAppsIndex !== -1) { + securitySubFeaturesList.splice(trustedAppsIndex + 1, 0, [ + SecuritySubFeatureId.trustedDevices, + enableSpaceAwarenessIfNeeded(trustedDevicesSubFeature()), + ]); + } + } + const securitySubFeaturesMap = new Map( securitySubFeaturesList ); diff --git a/x-pack/solutions/security/packages/kbn-securitysolution-io-ts-list-types/src/common/exception_list/index.ts b/x-pack/solutions/security/packages/kbn-securitysolution-io-ts-list-types/src/common/exception_list/index.ts index 50273a9c55a99..add02c6ee1217 100644 --- a/x-pack/solutions/security/packages/kbn-securitysolution-io-ts-list-types/src/common/exception_list/index.ts +++ b/x-pack/solutions/security/packages/kbn-securitysolution-io-ts-list-types/src/common/exception_list/index.ts @@ -11,6 +11,7 @@ export const exceptionListType = t.keyof({ detection: null, rule_default: null, endpoint: null, + endpoint_trusted_devices: null, endpoint_trusted_apps: null, endpoint_events: null, endpoint_host_isolation_exceptions: null, @@ -24,6 +25,7 @@ export enum ExceptionListTypeEnum { RULE_DEFAULT = 'rule_default', // rule default, cannot be shared ENDPOINT = 'endpoint', ENDPOINT_TRUSTED_APPS = 'endpoint', + ENDPOINT_TRUSTED_DEVICES = 'endpoint_trusted_devices', ENDPOINT_EVENTS = 'endpoint_events', ENDPOINT_HOST_ISOLATION_EXCEPTIONS = 'endpoint_host_isolation_exceptions', ENDPOINT_BLOCKLISTS = 'endpoint_blocklists', diff --git a/x-pack/solutions/security/packages/kbn-securitysolution-io-ts-list-types/src/request/internal/create_exception_list_schema/index.ts b/x-pack/solutions/security/packages/kbn-securitysolution-io-ts-list-types/src/request/internal/create_exception_list_schema/index.ts index ff8f9670a4e03..018e2804bfe5f 100644 --- a/x-pack/solutions/security/packages/kbn-securitysolution-io-ts-list-types/src/request/internal/create_exception_list_schema/index.ts +++ b/x-pack/solutions/security/packages/kbn-securitysolution-io-ts-list-types/src/request/internal/create_exception_list_schema/index.ts @@ -21,6 +21,7 @@ export const internalCreateExceptionListSchema = t.intersection([ endpoint_events: null, endpoint_host_isolation_exceptions: null, endpoint_blocklists: null, + endpoint_trusted_devices: null, }), }) ), diff --git a/x-pack/solutions/security/packages/kbn-securitysolution-list-constants/index.ts b/x-pack/solutions/security/packages/kbn-securitysolution-list-constants/index.ts index 32c5acbcdb886..12d5299ada969 100644 --- a/x-pack/solutions/security/packages/kbn-securitysolution-list-constants/index.ts +++ b/x-pack/solutions/security/packages/kbn-securitysolution-list-constants/index.ts @@ -80,6 +80,11 @@ export const ENDPOINT_ARTIFACT_LISTS = deepFreeze({ name: 'Endpoint Security Trusted Apps List', description: 'Endpoint Security Trusted Apps List', }, + trustedDevices: { + id: 'endpoint_trusted_devices', + name: 'Endpoint Security Trusted Devices List', + description: 'Endpoint Security Trusted Devices List', + }, eventFilters: { id: 'endpoint_event_filters', name: 'Endpoint Security Event Filters List', diff --git a/x-pack/solutions/security/packages/navigation/src/navigation_tree/assets_navigation_tree.ts b/x-pack/solutions/security/packages/navigation/src/navigation_tree/assets_navigation_tree.ts index 8d13869bd2fec..90e283fedbeb6 100644 --- a/x-pack/solutions/security/packages/navigation/src/navigation_tree/assets_navigation_tree.ts +++ b/x-pack/solutions/security/packages/navigation/src/navigation_tree/assets_navigation_tree.ts @@ -60,6 +60,10 @@ export const createAssetsNavigationTree = (core: CoreStart): NodeDefinition => ( id: SecurityPageName.trustedApps, link: securityLink(SecurityPageName.trustedApps), }, + { + id: SecurityPageName.trustedDevices, + link: securityLink(SecurityPageName.trustedDevices), + }, { id: SecurityPageName.eventFilters, link: securityLink(SecurityPageName.eventFilters), diff --git a/x-pack/solutions/security/plugins/lists/server/services/exception_lists/create_endpoint_trusted_devices_list.ts b/x-pack/solutions/security/plugins/lists/server/services/exception_lists/create_endpoint_trusted_devices_list.ts new file mode 100644 index 0000000000000..95a94275d8baa --- /dev/null +++ b/x-pack/solutions/security/plugins/lists/server/services/exception_lists/create_endpoint_trusted_devices_list.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectsClientContract, SavedObjectsErrorHelpers } from '@kbn/core/server'; +import { v4 as uuidv4 } from 'uuid'; +import type { Version } from '@kbn/securitysolution-io-ts-types'; +import type { ExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { getSavedObjectType } from '@kbn/securitysolution-list-utils'; +import { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants'; + +import { ExceptionListSoSchema } from '../../schemas/saved_objects'; + +import { transformSavedObjectToExceptionList } from './utils'; + +interface CreateEndpointListOptions { + savedObjectsClient: SavedObjectsClientContract; + user: string; + tieBreaker?: string; + version: Version; +} + +/** + * Creates the Endpoint Trusted Devices agnostic list if it does not yet exist + * + * @param savedObjectsClient + * @param user + * @param tieBreaker + * @param version + */ +// TODO: This function is a stub for future implementation of creating the Endpoint Trusted Devices list. It's not being executed in the current codebase. +export const createEndpointTrustedDevicesList = async ({ + savedObjectsClient, + user, + tieBreaker, + version, +}: CreateEndpointListOptions): Promise => { + const savedObjectType = getSavedObjectType({ namespaceType: 'agnostic' }); + const dateNow = new Date().toISOString(); + try { + const savedObject = await savedObjectsClient.create( + savedObjectType, + { + comments: undefined, + created_at: dateNow, + created_by: user, + description: ENDPOINT_ARTIFACT_LISTS.trustedDevices.description, + entries: undefined, + expire_time: undefined, + immutable: false, + item_id: undefined, + list_id: ENDPOINT_ARTIFACT_LISTS.trustedDevices.id, + list_type: 'list', + meta: undefined, + name: ENDPOINT_ARTIFACT_LISTS.trustedDevices.name, + os_types: [], + tags: [], + tie_breaker_id: tieBreaker ?? uuidv4(), + type: 'endpoint', + updated_by: user, + version, + }, + { + // We intentionally hard coding the id so that there can only be one Trusted devices list within the space + id: ENDPOINT_ARTIFACT_LISTS.trustedDevices.id, + } + ); + return transformSavedObjectToExceptionList({ savedObject }); + } catch (err) { + if (SavedObjectsErrorHelpers.isConflictError(err)) { + return null; + } else { + throw err; + } + } +}; diff --git a/x-pack/solutions/security/plugins/lists/server/services/exception_lists/exception_list_client.ts b/x-pack/solutions/security/plugins/lists/server/services/exception_lists/exception_list_client.ts index c344f6c637f5b..4762fc14da95e 100644 --- a/x-pack/solutions/security/plugins/lists/server/services/exception_lists/exception_list_client.ts +++ b/x-pack/solutions/security/plugins/lists/server/services/exception_lists/exception_list_client.ts @@ -79,6 +79,7 @@ import { findExceptionList } from './find_exception_list'; import { findExceptionListsItem } from './find_exception_list_items'; import { createEndpointList } from './create_endpoint_list'; import { createEndpointTrustedAppsList } from './create_endpoint_trusted_apps_list'; +import { createEndpointTrustedDevicesList } from './create_endpoint_trusted_devices_list'; import { PromiseFromStreams, importExceptions } from './import_exception_list_and_items'; import { transformCreateExceptionListItemOptionsToCreateExceptionListItemSchema, @@ -260,6 +261,21 @@ export class ExceptionListClient { }); }; + /** + * Create the Trusted Devices Agnostic list if it does not yet exist (`null` is returned if it does exist) + * @returns The exception list schema or null if it does not exist + */ + + // TODO: This is a stub for future implementation, it's not being executed in the current codebase. + public createTrustedDevicesList = async (): Promise => { + const { savedObjectsClient, user } = this; + return createEndpointTrustedDevicesList({ + savedObjectsClient, + user, + version: 1, + }); + }; + /** * This is the same as "createListItem" except it applies specifically to the agnostic endpoint list and will * auto-call the "createEndpointList" for you so that you have the best chance of the agnostic endpoint diff --git a/x-pack/solutions/security/plugins/security_solution/common/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/constants.ts index 672090e82667d..80f9321ac0e2d 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/constants.ts @@ -116,6 +116,7 @@ export const THREAT_INTELLIGENCE_PATH = '/threat_intelligence' as const; export const ENDPOINTS_PATH = `${MANAGEMENT_PATH}/endpoints` as const; export const POLICIES_PATH = `${MANAGEMENT_PATH}/policy` as const; export const TRUSTED_APPS_PATH = `${MANAGEMENT_PATH}/trusted_apps` as const; +export const TRUSTED_DEVICES_PATH = `${MANAGEMENT_PATH}/trusted_devices` as const; export const EVENT_FILTERS_PATH = `${MANAGEMENT_PATH}/event_filters` as const; export const HOST_ISOLATION_EXCEPTIONS_PATH = `${MANAGEMENT_PATH}/host_isolation_exceptions` as const; diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/authz/authz.test.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/authz/authz.test.ts index d7b631d6accb5..1c80989b3e163 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/authz/authz.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/authz/authz.test.ts @@ -169,6 +169,8 @@ describe('Endpoint Authz service', () => { ['canWriteFileOperations', 'writeFileOperations'], ['canWriteTrustedApplications', 'writeTrustedApplications'], ['canReadTrustedApplications', 'readTrustedApplications'], + ['canWriteTrustedDevices', 'writeTrustedDevices'], + ['canReadTrustedDevices', 'readTrustedDevices'], ['canWriteHostIsolationExceptions', 'writeHostIsolationExceptions'], ['canAccessHostIsolationExceptions', 'accessHostIsolationExceptions'], ['canReadHostIsolationExceptions', 'readHostIsolationExceptions'], @@ -211,6 +213,8 @@ describe('Endpoint Authz service', () => { ['canWriteFileOperations', ['writeFileOperations']], ['canWriteTrustedApplications', ['writeTrustedApplications']], ['canReadTrustedApplications', ['readTrustedApplications']], + ['canWriteTrustedDevices', ['writeTrustedDevices']], + ['canReadTrustedDevices', ['readTrustedDevices']], ['canWriteHostIsolationExceptions', ['writeHostIsolationExceptions']], ['canAccessHostIsolationExceptions', ['accessHostIsolationExceptions']], ['canReadHostIsolationExceptions', ['readHostIsolationExceptions']], @@ -264,6 +268,8 @@ describe('Endpoint Authz service', () => { ['canWriteFileOperations', ['writeFileOperations']], ['canWriteTrustedApplications', ['writeTrustedApplications']], ['canReadTrustedApplications', ['readTrustedApplications']], + ['canWriteTrustedDevices', ['writeTrustedDevices']], + ['canReadTrustedDevices', ['readTrustedDevices']], ['canWriteHostIsolationExceptions', ['writeHostIsolationExceptions']], ['canAccessHostIsolationExceptions', ['accessHostIsolationExceptions']], ['canReadHostIsolationExceptions', ['readHostIsolationExceptions']], @@ -361,8 +367,10 @@ describe('Endpoint Authz service', () => { canWriteFileOperations: false, canManageGlobalArtifacts: false, canWriteTrustedApplications: false, - canWriteWorkflowInsights: false, canReadTrustedApplications: false, + canWriteTrustedDevices: false, + canReadTrustedDevices: false, + canWriteWorkflowInsights: false, canReadWorkflowInsights: false, canWriteHostIsolationExceptions: false, canAccessHostIsolationExceptions: false, diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/authz/authz.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/authz/authz.ts index e4eb754e6bca9..d7d2f02cf34e2 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/authz/authz.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/service/authz/authz.ts @@ -82,6 +82,8 @@ export const calculateEndpointAuthz = ( const canWriteProcessOperations = hasAuth('writeProcessOperations'); const canWriteTrustedApplications = hasAuth('writeTrustedApplications'); const canReadTrustedApplications = hasAuth('readTrustedApplications'); + const canWriteTrustedDevices = hasAuth('writeTrustedDevices'); + const canReadTrustedDevices = hasAuth('readTrustedDevices'); const canWriteHostIsolationExceptions = hasAuth('writeHostIsolationExceptions'); const canReadHostIsolationExceptions = hasAuth('readHostIsolationExceptions'); const canAccessHostIsolationExceptions = hasAuth('accessHostIsolationExceptions'); @@ -153,6 +155,8 @@ export const calculateEndpointAuthz = ( // --------------------------------------------------------- canWriteTrustedApplications, canReadTrustedApplications, + canWriteTrustedDevices: canWriteTrustedDevices && isEnterpriseLicense, + canReadTrustedDevices: canReadTrustedDevices && isEnterpriseLicense, canWriteHostIsolationExceptions: canWriteHostIsolationExceptions && isPlatinumPlusLicense, canAccessHostIsolationExceptions: canAccessHostIsolationExceptions && isPlatinumPlusLicense, canReadHostIsolationExceptions, @@ -216,6 +220,8 @@ export const getEndpointAuthzInitialState = (): EndpointAuthz => { canWriteScanOperations: false, canWriteTrustedApplications: false, canReadTrustedApplications: false, + canWriteTrustedDevices: false, + canReadTrustedDevices: false, canWriteHostIsolationExceptions: false, canAccessHostIsolationExceptions: false, canReadHostIsolationExceptions: false, diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/authz.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/authz.ts index 262464aea0450..bdc25b8b1940a 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/authz.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/authz.ts @@ -64,6 +64,10 @@ export interface EndpointAuthz { canWriteTrustedApplications: boolean; /** If the user has read permissions for trusted applications */ canReadTrustedApplications: boolean; + /** If the user has write permissions for trusted devices */ + canWriteTrustedDevices: boolean; + /** If the user has read permissions for trusted devices */ + canReadTrustedDevices: boolean; /** If the user has write permissions for host isolation exceptions */ canWriteHostIsolationExceptions: boolean; /** If the user has read permissions for host isolation exceptions */ diff --git a/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts b/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts index 94dd521c06162..50e22ddfebd30 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts @@ -274,6 +274,12 @@ export const allowedExperimentalValues = Object.freeze({ * Enables advanced mode for Trusted Apps creation and update */ trustedAppsAdvancedMode: false, + + /** + * Enables Trusted Devices artifact management for device control protections. + * Allows users to manage trusted USB and external devices + */ + trustedDevicesEnabled: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/solutions/security/plugins/security_solution/public/app/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/app/translations.ts index 146fa31a8b70e..7f315a90c1822 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/app/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/app/translations.ts @@ -164,6 +164,12 @@ export const TRUSTED_APPLICATIONS = i18n.translate( defaultMessage: 'Trusted applications', } ); +export const TRUSTED_DEVICES = i18n.translate( + 'xpack.securitySolution.search.administration.trustedDevices', + { + defaultMessage: 'Trusted devices', + } +); export const EVENT_FILTERS = i18n.translate( 'xpack.securitySolution.search.administration.eventFilters', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/common/breadcrumbs.ts b/x-pack/solutions/security/plugins/security_solution/public/management/common/breadcrumbs.ts index 82b321abdcd6e..ea7b68125c2b7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/common/breadcrumbs.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/common/breadcrumbs.ts @@ -7,7 +7,13 @@ import type { ChromeBreadcrumb } from '@kbn/core/public'; import { AdministrationSubTab } from '../types'; -import { ENDPOINTS_TAB, EVENT_FILTERS_TAB, POLICIES_TAB, TRUSTED_APPS_TAB } from './translations'; +import { + ENDPOINTS_TAB, + EVENT_FILTERS_TAB, + POLICIES_TAB, + TRUSTED_APPS_TAB, + TRUSTED_DEVICES_TAB, +} from './translations'; import type { AdministrationRouteSpyState } from '../../common/utils/route/types'; import { HOST_ISOLATION_EXCEPTIONS, @@ -21,6 +27,7 @@ const TabNameMappedToI18nKey: Record = { [AdministrationSubTab.endpoints]: ENDPOINTS_TAB, [AdministrationSubTab.policies]: POLICIES_TAB, [AdministrationSubTab.trustedApps]: TRUSTED_APPS_TAB, + [AdministrationSubTab.trustedDevices]: TRUSTED_DEVICES_TAB, [AdministrationSubTab.eventFilters]: EVENT_FILTERS_TAB, [AdministrationSubTab.hostIsolationExceptions]: HOST_ISOLATION_EXCEPTIONS, [AdministrationSubTab.blocklist]: BLOCKLIST, diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/common/constants.ts b/x-pack/solutions/security/plugins/security_solution/public/management/common/constants.ts index b319adbd0faeb..6dfeace27c2f3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/common/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/common/constants.ts @@ -14,6 +14,7 @@ export const MANAGEMENT_ROUTING_ENDPOINTS_PATH = `${MANAGEMENT_PATH}/:tabName(${ export const MANAGEMENT_ROUTING_POLICIES_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})`; export const MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId/settings`; export const MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId/trustedApps`; +export const MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_DEVICES_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId/trustedDevices`; export const MANAGEMENT_ROUTING_POLICY_DETAILS_EVENT_FILTERS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId/eventFilters`; export const MANAGEMENT_ROUTING_POLICY_DETAILS_HOST_ISOLATION_EXCEPTIONS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId/hostIsolationExceptions`; export const MANAGEMENT_ROUTING_POLICY_DETAILS_BLOCKLISTS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId/blocklists`; @@ -22,6 +23,7 @@ export const MANAGEMENT_ROUTING_NOTES_PATH = `${MANAGEMENT_PATH}/:tabName(${Admi /** @deprecated use the paths defined above instead */ export const MANAGEMENT_ROUTING_POLICY_DETAILS_PATH_OLD = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId`; export const MANAGEMENT_ROUTING_TRUSTED_APPS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.trustedApps})`; +export const MANAGEMENT_ROUTING_TRUSTED_DEVICES_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.trustedDevices})`; export const MANAGEMENT_ROUTING_EVENT_FILTERS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.eventFilters})`; export const MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.hostIsolationExceptions})`; export const MANAGEMENT_ROUTING_BLOCKLIST_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.blocklist})`; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/common/routing.ts b/x-pack/solutions/security/plugins/security_solution/public/management/common/routing.ts index dc662a68dcd29..92e9b51b84537 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/common/routing.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/common/routing.ts @@ -31,7 +31,9 @@ import { MANAGEMENT_ROUTING_POLICY_DETAILS_HOST_ISOLATION_EXCEPTIONS_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_PROTECTION_UPDATES_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH, + MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_DEVICES_PATH, MANAGEMENT_ROUTING_TRUSTED_APPS_PATH, + MANAGEMENT_ROUTING_TRUSTED_DEVICES_PATH, } from './constants'; import { isDefaultOrMissing, getArtifactListPageUrlPath } from './url_routing'; @@ -137,6 +139,18 @@ export const getPolicyTrustedAppsPath = (policyId: string, search?: string) => { })}${appendSearch(search)}`; }; +export const getPolicyTrustedDevicesPath = ( + policyId: string, + location?: Partial +) => { + return `${generatePath(MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_DEVICES_PATH, { + tabName: AdministrationSubTab.policies, + policyId, + })}${appendSearch( + querystring.stringify(normalizePolicyDetailsArtifactsListPageLocation(location)) + )}`; +}; + export const getPolicyEventFiltersPath = ( policyId: string, location?: Partial @@ -212,6 +226,16 @@ export const getTrustedAppsListPath = (location?: Partial +): string => { + const path = generatePath(MANAGEMENT_ROUTING_TRUSTED_DEVICES_PATH, { + tabName: AdministrationSubTab.trustedDevices, + }); + + return getArtifactListPageUrlPath(path, location); +}; + export const extractPolicyDetailsArtifactsListPageLocation = ( query: querystring.ParsedUrlQuery ): PolicyDetailsArtifactsPageLocation => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/common/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/common/translations.ts index b4977fb403afb..8cfd1da06a3f8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/common/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/common/translations.ts @@ -20,6 +20,10 @@ export const TRUSTED_APPS_TAB = i18n.translate('xpack.securitySolution.trustedAp defaultMessage: 'Trusted applications', }); +export const TRUSTED_DEVICES_TAB = i18n.translate('xpack.securitySolution.trustedDevicesTab', { + defaultMessage: 'Trusted devices', +}); + export const EVENT_FILTERS_TAB = i18n.translate('xpack.securitySolution.eventFiltersTab', { defaultMessage: 'Event filters', }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/links.ts b/x-pack/solutions/security/plugins/security_solution/public/management/links.ts index c9f541317d73f..dbd5c3c71506b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/links.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/links.ts @@ -26,6 +26,7 @@ import { SecurityPageName, SECURITY_FEATURE_ID, TRUSTED_APPS_PATH, + TRUSTED_DEVICES_PATH, } from '../../common/constants'; import { BLOCKLIST, @@ -36,6 +37,7 @@ import { POLICIES, RESPONSE_ACTIONS_HISTORY, TRUSTED_APPLICATIONS, + TRUSTED_DEVICES, ENTITY_ANALYTICS_RISK_SCORE, ENTITY_STORE, } from '../app/translations'; @@ -72,6 +74,7 @@ const categories = [ SecurityPageName.endpoints, SecurityPageName.policies, SecurityPageName.trustedApps, + SecurityPageName.trustedDevices, SecurityPageName.eventFilters, SecurityPageName.hostIsolationExceptions, SecurityPageName.blocklist, @@ -139,6 +142,21 @@ export const links: LinkItem = { skipUrlState: true, hideTimeline: true, }, + { + id: SecurityPageName.trustedDevices, + title: TRUSTED_DEVICES, + description: i18n.translate('xpack.securitySolution.appLinks.trustedDevicesDescription', { + defaultMessage: + 'Add a trusted device to improve performance or alleviate compatibility issues.', + }), + landingIcon: IconDashboards, + path: TRUSTED_DEVICES_PATH, + skipUrlState: true, + hideTimeline: true, + experimentalKey: 'trustedDevicesEnabled', + capabilities: [`${SECURITY_FEATURE_ID}.readTrustedDevices`], + licenseType: 'enterprise', + }, { id: SecurityPageName.eventFilters, title: EVENT_FILTERS, @@ -230,6 +248,7 @@ export const getManagementFilteredLinks = async ( canReadHostIsolationExceptions, canReadEndpointList, canReadTrustedApplications, + canReadTrustedDevices, canReadEventFilters, canReadBlocklist, canReadPolicyManagement, @@ -267,6 +286,10 @@ export const getManagementFilteredLinks = async ( linksToExclude.push(SecurityPageName.trustedApps); } + if (!canReadTrustedDevices) { + linksToExclude.push(SecurityPageName.trustedDevices); + } + if (!canReadEventFilters) { linksToExclude.push(SecurityPageName.eventFilters); } diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/index.tsx index 5ebaf0f970582..8a96bb656cfb4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/index.tsx @@ -20,6 +20,7 @@ import { MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH, MANAGEMENT_ROUTING_POLICIES_PATH, MANAGEMENT_ROUTING_TRUSTED_APPS_PATH, + MANAGEMENT_ROUTING_TRUSTED_DEVICES_PATH, MANAGEMENT_ROUTING_BLOCKLIST_PATH, MANAGEMENT_ROUTING_RESPONSE_ACTIONS_HISTORY_PATH, MANAGEMENT_ROUTING_NOTES_PATH, @@ -38,6 +39,7 @@ import { BlocklistContainer } from './blocklist'; import { ResponseActionsContainer } from './response_actions'; import { PrivilegedRoute } from '../components/privileged_route'; import { SecurityRoutePageWrapper } from '../../common/components/security_route_page_wrapper'; +import { TrustedDevicesContainer } from './trusted_devices'; const EndpointTelemetry = () => ( @@ -60,6 +62,13 @@ const TrustedAppTelemetry = () => ( ); +const TrustedDevicesTelemetry = () => ( + + + + +); + const EventFilterTelemetry = () => ( @@ -92,11 +101,14 @@ export const ManagementContainer = memo(() => { 'securitySolutionNotesDisabled' ); + const trustedDevicesEnabled = useIsExperimentalFeatureEnabled('trustedDevicesEnabled'); + const { loading, canReadPolicyManagement, canReadBlocklist, canReadTrustedApplications, + canReadTrustedDevices, canReadEventFilters, canReadActionsLogManagement, canReadEndpointList, @@ -141,6 +153,13 @@ export const ManagementContainer = memo(() => { component={TrustedAppTelemetry} hasPrivilege={canReadTrustedApplications} /> + {trustedDevicesEnabled && ( + + )} { const isProtectionUpdatesFeatureEnabled = useIsExperimentalFeatureEnabled( 'protectionUpdatesEnabled' ); + const isTrustedDevicesFeatureEnabled = useIsExperimentalFeatureEnabled('trustedDevicesEnabled'); const isEnterprise = useLicense().isEnterprise(); const isProtectionUpdatesEnabled = isEnterprise && isProtectionUpdatesFeatureEnabled; @@ -38,6 +40,9 @@ export const PolicyContainer = memo(() => { path={[ MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH, + ...(isTrustedDevicesFeatureEnabled + ? [MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_DEVICES_PATH] + : []), MANAGEMENT_ROUTING_POLICY_DETAILS_EVENT_FILTERS_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_HOST_ISOLATION_EXCEPTIONS_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_BLOCKLISTS_PATH, diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_common_selectors.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_common_selectors.ts index eb081d10d6375..b051b2e4bcf29 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_common_selectors.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_common_selectors.ts @@ -14,6 +14,7 @@ import { MANAGEMENT_ROUTING_POLICY_DETAILS_EVENT_FILTERS_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_BLOCKLISTS_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_PROTECTION_UPDATES_PATH, + MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_DEVICES_PATH, } from '../../../../../common/constants'; import type { PolicyDetailsSelector, PolicyDetailsState } from '../../../types'; @@ -53,6 +54,19 @@ export const isOnPolicyTrustedAppsView: PolicyDetailsSelector = createS } ); +/** Returns a boolean of whether the user is on the policy trusted devices page or not */ +export const isOnPolicyTrustedDevicesView: PolicyDetailsSelector = createSelector( + getUrlLocationPathname, + (pathname) => { + return ( + matchPath(pathname ?? '', { + path: MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_DEVICES_PATH, + exact: true, + }) !== null + ); + } +); + /** Returns a boolean of whether the user is on the policy event filters page or not */ export const isOnPolicyEventFiltersView: PolicyDetailsSelector = createSelector( getUrlLocationPathname, diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts index 7e79fb9c57cdd..3d9bf6f6d8b5b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts @@ -25,6 +25,7 @@ import { MANAGEMENT_ROUTING_POLICY_DETAILS_EVENT_FILTERS_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_BLOCKLISTS_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_PROTECTION_UPDATES_PATH, + MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_DEVICES_PATH, } from '../../../../../common/constants'; import type { ManagementRoutePolicyDetailsParams } from '../../../../../types'; import { getPolicyDataForUpdate } from '../../../../../../../common/endpoint/service/policy'; @@ -33,6 +34,7 @@ import { isOnPolicyEventFiltersView, isOnHostIsolationExceptionsView, isOnPolicyFormView, + isOnPolicyTrustedDevicesView, isOnBlocklistsView, isOnProtectionUpdatesView, } from './policy_common_selectors'; @@ -96,6 +98,7 @@ export const needsToRefresh = (state: Immutable): boolean => export const isOnPolicyDetailsPage = (state: Immutable) => isOnPolicyFormView(state) || isOnPolicyTrustedAppsView(state) || + isOnPolicyTrustedDevicesView(state) || isOnPolicyEventFiltersView(state) || isOnHostIsolationExceptionsView(state) || isOnBlocklistsView(state) || @@ -115,6 +118,7 @@ export const policyIdFromParams: (state: Immutable) => strin path: [ MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH, + MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_DEVICES_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_EVENT_FILTERS_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_HOST_ISOLATION_EXCEPTIONS_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_BLOCKLISTS_PATH, diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx index 6e72dc260d64a..2e1c2ff4d54cb 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx @@ -29,7 +29,12 @@ import { EVENT_FILTERS_LABELS, HOST_ISOLATION_EXCEPTIONS_LABELS, TRUSTED_APPS_LABELS, + TRUSTED_DEVICES_LABELS, } from './translations'; +import { useLicense } from '../../../../../../common/hooks/use_license'; + +import { TrustedDevicesApiClient } from '../../../../trusted_devices/service/api_client'; +import { useIsExperimentalFeatureEnabled } from '../../../../../../common/hooks/use_experimental_features'; const TrustedAppsArtifactCard = memo((props) => { const http = useHttp(); @@ -50,6 +55,25 @@ const TrustedAppsArtifactCard = memo((prop }); TrustedAppsArtifactCard.displayName = 'TrustedAppsArtifactCard'; +const TrustedDevicesArtifactCard = memo((props) => { + const http = useHttp(); + const trustedDevicesApiClientInstance = useMemo( + () => TrustedDevicesApiClient.getInstance(http), + [http] + ); + + return ( + + ); +}); +TrustedDevicesArtifactCard.displayName = 'TrustedDevicesArtifactCard'; + const EventFiltersArtifactCard = memo((props) => { const http = useHttp(); const eventFiltersApiClientInstance = useMemo( @@ -67,6 +91,7 @@ const EventFiltersArtifactCard = memo((pro /> ); }); + EventFiltersArtifactCard.displayName = 'EventFiltersArtifactCard'; const HostIsolationExceptionsArtifactCard = memo((props) => { @@ -115,9 +140,15 @@ export const EndpointPackageCustomExtension = memo { if (loading) { @@ -137,6 +168,13 @@ export const EndpointPackageCustomExtension = memo )} + {trustedDevicesVisible && ( + <> + + + + )} + {canReadEventFilters && ( <> @@ -155,13 +193,14 @@ export const EndpointPackageCustomExtension = memo ); }, [ - canReadBlocklist, - canReadEventFilters, - canReadTrustedApplications, - canReadHostIsolationExceptions, loading, - props, userCanAccessContent, + canReadTrustedApplications, + props, + trustedDevicesVisible, + canReadEventFilters, + canReadHostIsolationExceptions, + canReadBlocklist, ]); if (loading) { diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/translations.tsx index 31d9f6256608b..b0b4d6b742902 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/translations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/translations.tsx @@ -26,6 +26,23 @@ export const TRUSTED_APPS_LABELS = { ), }; +export const TRUSTED_DEVICES_LABELS = { + artifactsSummaryApiError: (error: string) => + i18n.translate( + 'xpack.securitySolution.endpoint.fleetCustomExtension.trustedDevicesSummary.error', + { + defaultMessage: 'There was an error trying to fetch trusted devices stats: "{error}"', + values: { error }, + } + ), + cardTitle: ( + + ), +}; + export const EVENT_FILTERS_LABELS = { artifactsSummaryApiError: (error: string) => i18n.translate( diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/components/endpoint_policy_artifact_cards.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/components/endpoint_policy_artifact_cards.tsx index bbb4258ccc2da..fe8df6746ffd4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/components/endpoint_policy_artifact_cards.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/components/endpoint_policy_artifact_cards.tsx @@ -14,9 +14,11 @@ import { EVENT_FILTERS_LABELS, HOST_ISOLATION_EXCEPTIONS_LABELS, TRUSTED_APPS_LABELS, + TRUSTED_DEVICES_LABELS, } from '../translations'; import { useCanAccessSomeArtifacts } from '../../hooks/use_can_access_some_artifacts'; import { BlocklistsApiClient } from '../../../../../blocklist/services'; +import { TrustedDevicesApiClient } from '../../../../../trusted_devices/service/api_client'; import { HostIsolationExceptionsApiClient } from '../../../../../host_isolation_exceptions/host_isolation_exceptions_api_client'; import { EventFiltersApiClient } from '../../../../../event_filters/service/api_client'; import { TrustedAppsApiClient } from '../../../../../trusted_apps/service'; @@ -28,7 +30,9 @@ import { getPolicyEventFiltersPath, getPolicyHostIsolationExceptionsPath, getPolicyTrustedAppsPath, + getPolicyTrustedDevicesPath, getTrustedAppsListPath, + getTrustedDevicesListPath, } from '../../../../../../common/routing'; import { SEARCHABLE_FIELDS as TRUSTED_APPS_SEARCHABLE_FIELDS } from '../../../../../trusted_apps/constants'; import type { FleetIntegrationArtifactCardProps } from './fleet_integration_artifacts_card'; @@ -36,7 +40,10 @@ import { FleetIntegrationArtifactsCard } from './fleet_integration_artifacts_car import { SEARCHABLE_FIELDS as EVENT_FILTERS_SEARCHABLE_FIELDS } from '../../../../../event_filters/constants'; import { SEARCHABLE_FIELDS as HOST_ISOLATION_EXCEPTIONS_SEARCHABLE_FIELDS } from '../../../../../host_isolation_exceptions/constants'; import { SEARCHABLE_FIELDS as BLOCKLIST_SEARCHABLE_FIELDS } from '../../../../../blocklist/constants'; +import { SEARCHABLE_FIELDS as TRUSTED_DEVICES_SEARCHABLE_FIELDS } from '../../../../../trusted_devices/constants'; +import { useIsExperimentalFeatureEnabled } from '../../../../../../../common/hooks/use_experimental_features'; import { useHttp } from '../../../../../../../common/lib/kibana'; +import { useLicense } from '../../../../../../../common/hooks/use_license'; interface PolicyArtifactCardProps { policyId: string; @@ -72,6 +79,36 @@ const TrustedAppsPolicyCard = memo(({ policyId }) => { }); TrustedAppsPolicyCard.displayName = 'TrustedAppsPolicyCard'; +const TrustedDevicesPolicyCard = memo(({ policyId }) => { + const http = useHttp(); + const trustedDevicesApiClientInstance = useMemo( + () => TrustedDevicesApiClient.getInstance(http), + [http] + ); + const { canReadPolicyManagement } = useUserPrivileges().endpointPrivileges; + + const getArtifactPathHandler: FleetIntegrationArtifactCardProps['getArtifactsPath'] = + useCallback(() => { + if (canReadPolicyManagement) { + return getPolicyTrustedDevicesPath(policyId); + } + + return getTrustedDevicesListPath({ includedPolicies: `${policyId},global` }); + }, [canReadPolicyManagement, policyId]); + + return ( + + ); +}); +TrustedDevicesPolicyCard.displayName = 'TrustedDevicesPolicyCard'; + const EventFiltersPolicyCard = memo(({ policyId }) => { const http = useHttp(); const eventFiltersApiClientInstance = useMemo( @@ -174,8 +211,15 @@ export const EndpointPolicyArtifactCards = memo; @@ -205,6 +249,13 @@ export const EndpointPolicyArtifactCards = memo )} + {trustedDevicesVisible && ( + <> + + + + )} + {canReadEventFilters && ( <> diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/translations.tsx index c99ea5bf3b303..2067997c514cd 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/translations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/translations.tsx @@ -95,3 +95,26 @@ export const TRUSTED_APPS_LABELS = { /> ), }; + +export const TRUSTED_DEVICES_LABELS = { + artifactsSummaryApiError: (error: string) => + i18n.translate( + 'xpack.securitySolution.endpoint.fleetIntegrationCard.trustedDevicesSummary.error', + { + defaultMessage: 'There was an error trying to fetch trusted devices stats: "{error}"', + values: { error }, + } + ), + cardTitle: ( + + ), + linkLabel: ( + + ), +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts index 19c3d9d6f3ea6..16ce12c9f9e30 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts @@ -9,6 +9,7 @@ import { useCallback } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { useSelector } from 'react-redux'; import { + ENDPOINT_ARTIFACT_LISTS, ENDPOINT_BLOCKLISTS_LIST_ID, ENDPOINT_EVENT_FILTERS_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID, @@ -24,6 +25,7 @@ import { getPolicyDetailsArtifactsListPath, getPolicyEventFiltersPath, getPolicyHostIsolationExceptionsPath, + getPolicyTrustedDevicesPath, } from '../../../common/routing'; import { getCurrentArtifactsLocation, policyIdFromParams } from '../store/policy_details/selectors'; import { POLICIES_PATH } from '../../../../../common/constants'; @@ -56,6 +58,11 @@ export function usePolicyDetailsArtifactsNavigateCallback(listId: string) { ...location, ...args, }); + } else if (listId === ENDPOINT_ARTIFACT_LISTS.trustedDevices.id) { + return getPolicyTrustedDevicesPath(policyId, { + ...location, + ...args, + }); } else if (listId === ENDPOINT_EVENT_FILTERS_LIST_ID) { return getPolicyEventFiltersPath(policyId, { ...location, diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx index c350f91c914d6..ae031b8610a9c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx @@ -30,6 +30,8 @@ import { getBlocklistsListPath, getPolicyBlocklistsPath, getPolicyProtectionUpdatesPath, + getTrustedDevicesListPath, + getPolicyTrustedDevicesPath, } from '../../../../common/routing'; import { useHttp, useToasts } from '../../../../../common/lib/kibana'; import { ManagementPageLoader } from '../../../../components/management_page_loader'; @@ -42,6 +44,7 @@ import { policyDetails, policyIdFromParams, isOnProtectionUpdatesView, + isOnPolicyTrustedDevicesView, } from '../../store/policy_details/selectors'; import { PolicyArtifactsLayout } from '../artifacts/layout/policy_artifacts_layout'; import { usePolicyDetailsSelector } from '../policy_hooks'; @@ -57,8 +60,11 @@ import { SEARCHABLE_FIELDS as TRUSTED_APPS_SEARCHABLE_FIELDS } from '../../../tr import { SEARCHABLE_FIELDS as EVENT_FILTERS_SEARCHABLE_FIELDS } from '../../../event_filters/constants'; import { SEARCHABLE_FIELDS as HOST_ISOLATION_EXCEPTIONS_SEARCHABLE_FIELDS } from '../../../host_isolation_exceptions/constants'; import { SEARCHABLE_FIELDS as BLOCKLISTS_SEARCHABLE_FIELDS } from '../../../blocklist/constants'; +import { SEARCHABLE_FIELDS as TRUSTED_DEVICES_SEARCHABLE_FIELDS } from '../../../trusted_devices/constants'; import type { PolicyDetailsRouteState } from '../../../../../../common/endpoint/types'; import { useHostIsolationExceptionsAccess } from '../../../../hooks/artifacts/use_host_isolation_exceptions_access'; +import { TrustedDevicesApiClient } from '../../../trusted_devices/service/api_client'; +import { POLICY_ARTIFACT_TRUSTED_DEVICES_LABELS } from './trusted_devices_translations'; enum PolicyTabKeys { SETTINGS = 'settings', @@ -67,6 +73,7 @@ enum PolicyTabKeys { HOST_ISOLATION_EXCEPTIONS = 'hostIsolationExceptions', BLOCKLISTS = 'blocklists', PROTECTION_UPDATES = 'protectionUpdates', + TRUSTED_DEVICES = 'trustedDevices', } interface PolicyTab { @@ -82,6 +89,7 @@ export const PolicyTabs = React.memo(() => { const isInSettingsTab = usePolicyDetailsSelector(isOnPolicyFormView); const isInTrustedAppsTab = usePolicyDetailsSelector(isOnPolicyTrustedAppsView); + const isInTrustedDevicesTab = usePolicyDetailsSelector(isOnPolicyTrustedDevicesView); const isInEventFiltersTab = usePolicyDetailsSelector(isOnPolicyEventFiltersView); const isInHostIsolationExceptionsTab = usePolicyDetailsSelector(isOnHostIsolationExceptionsView); const isInBlocklistsTab = usePolicyDetailsSelector(isOnBlocklistsView); @@ -124,6 +132,8 @@ export const PolicyTabs = React.memo(() => { canWriteHostIsolationExceptions, canReadBlocklist, canWriteBlocklist, + canReadTrustedDevices, + canWriteTrustedDevices, loading: isPrivilegesLoading, } = useUserPrivileges().endpointPrivileges; const { state: routeState = {} } = useLocation(); @@ -131,8 +141,12 @@ export const PolicyTabs = React.memo(() => { const isProtectionUpdatesFeatureEnabled = useIsExperimentalFeatureEnabled( 'protectionUpdatesEnabled' ); + const isTrustedDevicesFeatureEnabled = useIsExperimentalFeatureEnabled('trustedDevicesEnabled'); + const isEnterprise = useLicense().isEnterprise(); const isProtectionUpdatesEnabled = isEnterprise && isProtectionUpdatesFeatureEnabled; + const isTrustedDevicesEnabled = + isEnterprise && isTrustedDevicesFeatureEnabled && canReadTrustedDevices; const getHostIsolationExceptionsApiClientInstance = useCallback( () => HostIsolationExceptionsApiClient.getInstance(http), @@ -161,7 +175,8 @@ export const PolicyTabs = React.memo(() => { (isInTrustedAppsTab && !canReadTrustedApplications) || (isInEventFiltersTab && !canReadEventFilters) || redirectHostIsolationException || - (isInBlocklistsTab && !canReadBlocklist) + (isInBlocklistsTab && !canReadBlocklist) || + (isInTrustedDevicesTab && !canReadTrustedDevices) ) { history.replace(getPolicyDetailPath(policyId)); toasts.addDanger( @@ -176,6 +191,7 @@ export const PolicyTabs = React.memo(() => { canReadEventFilters, canReadHostIsolationExceptions, canReadTrustedApplications, + canReadTrustedDevices, hasAccessToHostIsolationExceptions, history, isHostIsolationExceptionsAccessLoading, @@ -184,6 +200,7 @@ export const PolicyTabs = React.memo(() => { isInHostIsolationExceptionsTab, isInProtectionUpdatesTab, isInTrustedAppsTab, + isInTrustedDevicesTab, isPrivilegesLoading, policyId, toasts, @@ -194,6 +211,11 @@ export const PolicyTabs = React.memo(() => { [http] ); + const getTrustedDevicesApiClientInstance = useCallback( + () => TrustedDevicesApiClient.getInstance(http), + [http] + ); + const getEventFiltersApiClientInstance = useCallback( () => EventFiltersApiClient.getInstance(http), [http] @@ -216,6 +238,17 @@ export const PolicyTabs = React.memo(() => { ), }; + const trustedDevicesLabels = { + ...POLICY_ARTIFACT_TRUSTED_DEVICES_LABELS, + layoutAboutMessage: (count: number, link: React.ReactElement): React.ReactNode => ( + + ), + }; + const eventFiltersLabels = { ...POLICY_ARTIFACT_EVENT_FILTERS_LABELS, layoutAboutMessage: (count: number, link: React.ReactElement): React.ReactNode => ( @@ -293,6 +326,32 @@ export const PolicyTabs = React.memo(() => { 'data-test-subj': 'policyTrustedAppsTab', } : undefined, + [PolicyTabKeys.TRUSTED_DEVICES]: isTrustedDevicesEnabled + ? { + id: PolicyTabKeys.TRUSTED_DEVICES, + name: i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.tabs.trustedDevices', + { + defaultMessage: 'Trusted devices', + } + ), + content: ( + <> + + + + ), + 'data-test-subj': 'policyTrustedDevicesTab', + } + : undefined, [PolicyTabKeys.EVENT_FILTERS]: canReadEventFilters ? { id: PolicyTabKeys.EVENT_FILTERS, @@ -398,6 +457,9 @@ export const PolicyTabs = React.memo(() => { canReadTrustedApplications, getTrustedAppsApiClientInstance, canWriteTrustedApplications, + isTrustedDevicesEnabled, + getTrustedDevicesApiClientInstance, + canWriteTrustedDevices, canReadEventFilters, getEventFiltersApiClientInstance, canWriteEventFilters, @@ -424,6 +486,8 @@ export const PolicyTabs = React.memo(() => { selectedTab = tabs[PolicyTabKeys.SETTINGS]; } else if (isInTrustedAppsTab) { selectedTab = tabs[PolicyTabKeys.TRUSTED_APPS]; + } else if (isInTrustedDevicesTab) { + selectedTab = tabs[PolicyTabKeys.TRUSTED_DEVICES]; } else if (isInEventFiltersTab) { selectedTab = tabs[PolicyTabKeys.EVENT_FILTERS]; } else if (isInHostIsolationExceptionsTab) { @@ -439,6 +503,7 @@ export const PolicyTabs = React.memo(() => { tabs, isInSettingsTab, isInTrustedAppsTab, + isInTrustedDevicesTab, isInEventFiltersTab, isInHostIsolationExceptionsTab, isInBlocklistsTab, @@ -462,6 +527,9 @@ export const PolicyTabs = React.memo(() => { case PolicyTabKeys.TRUSTED_APPS: path = getPolicyTrustedAppsPath(policyId); break; + case PolicyTabKeys.TRUSTED_DEVICES: + path = getPolicyTrustedDevicesPath(policyId); + break; case PolicyTabKeys.EVENT_FILTERS: path = getPolicyEventFiltersPath(policyId); break; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/trusted_devices_translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/trusted_devices_translations.ts new file mode 100644 index 0000000000000..9bf82bcfa0457 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/trusted_devices_translations.ts @@ -0,0 +1,166 @@ +/* + * 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 { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; + +export const POLICY_ARTIFACT_TRUSTED_DEVICES_LABELS = Object.freeze({ + deleteModalTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedDevices.list.removeDialog.title', + { + defaultMessage: 'Remove trusted device from policy', + } + ), + deleteModalImpactInfo: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedDevices.list.removeDialog.messageCallout', + { + defaultMessage: + 'This trusted device will be removed only from this policy and can still be found and managed from the artifact page.', + } + ), + deleteModalErrorMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedDevices.list.removeDialog.errorToastTitle', + { + defaultMessage: 'Error while attempting to remove trusted device', + } + ), + flyoutWarningCalloutMessage: (maxNumber: number) => + i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedDevices.layout.flyout.searchWarning.text', + { + defaultMessage: + 'Only the first {maxNumber} trusted devices are displayed. Please use the search bar to refine the results.', + values: { maxNumber }, + } + ), + flyoutNoArtifactsToBeAssignedMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedDevices.layout.flyout.noAssignable', + { + defaultMessage: 'There are no trusted devices that can be assigned to this policy.', + } + ), + flyoutTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedDevices.layout.flyout.title', + { + defaultMessage: 'Assign trusted devices', + } + ), + flyoutSubtitle: (policyName: string): string => + i18n.translate('xpack.securitySolution.endpoint.policy.trustedDevices.layout.flyout.subtitle', { + defaultMessage: 'Select trusted devices to add to {policyName}', + values: { policyName }, + }), + flyoutSearchPlaceholder: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedDevices.layout.search.label', + { + defaultMessage: 'Search trusted devices', + } + ), + flyoutErrorMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedDevices.layout.flyout.toastError.text', + { + defaultMessage: `An error occurred updating trusted devices`, + } + ), + flyoutSuccessMessageText: (updatedExceptions: ExceptionListItemSchema[]): string => + updatedExceptions.length > 1 + ? i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedDevices.layout.flyout.toastSuccess.textMultiples', + { + defaultMessage: '{count} trusted devices have been added to your list.', + values: { count: updatedExceptions.length }, + } + ) + : i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedDevices.layout.flyout.toastSuccess.textSingle', + { + defaultMessage: '"{name}" has been added to your trusted device list.', + values: { name: updatedExceptions[0].name }, + } + ), + emptyUnassignedTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedDevices.empty.unassigned.title', + { defaultMessage: 'No assigned trusted devices' } + ), + emptyUnassignedMessage: (policyName: string): string => + i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedDevices.empty.unassigned.content', + { + defaultMessage: + 'There are currently no trusted devices assigned to {policyName}. Assign trusted devices now or add and manage them on the trusted devices page.', + values: { policyName }, + } + ), + emptyUnassignedPrimaryActionButtonTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedDevices.empty.unassigned.primaryAction', + { + defaultMessage: 'Assign trusted devices', + } + ), + emptyUnassignedSecondaryActionButtonTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedDevices.empty.unassigned.secondaryAction', + { + defaultMessage: 'Manage trusted devices', + } + ), + emptyUnassignedNoPrivilegesMessage: (policyName: string): string => + i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedDevices.empty.unassigned.noPrivileges.content', + { + defaultMessage: 'There are currently no trusted devices assigned to {policyName}.', + values: { policyName }, + } + ), + emptyUnexistingTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedDevices.empty.unexisting.title', + { defaultMessage: 'No trusted devices exist' } + ), + emptyUnexistingMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedDevices.empty.unexisting.content', + { defaultMessage: 'There are currently no trusted devices applied to your endpoints.' } + ), + emptyUnexistingPrimaryActionButtonTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedDevices.empty.unexisting.action', + { defaultMessage: 'Add trusted devices' } + ), + listTotalItemCountMessage: (totalItemsCount: number): string => + i18n.translate('xpack.securitySolution.endpoint.policy.trustedDevices.list.totalItemCount', { + defaultMessage: + 'Showing {totalItemsCount, plural, one {# trusted device} other {# trusted devices}}', + values: { totalItemsCount }, + }), + listRemoveActionNotAllowedMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedDevices.list.removeActionNotAllowed', + { + defaultMessage: 'Globally applied trusted device cannot be removed from policy.', + } + ), + listSearchPlaceholderMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedDevices.list.search.placeholder', + { + defaultMessage: `Search on the fields below: name, description, value`, + } + ), + layoutTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedDevices.layout.title', + { + defaultMessage: 'Assigned trusted devices', + } + ), + layoutAssignButtonTitle: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedDevices.layout.assignToPolicy', + { + defaultMessage: 'Assign trusted devices to policy', + } + ), + layoutViewAllLinkMessage: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedDevices.layout.about.viewAllLinkLabel', + { + defaultMessage: 'view all trusted devices', + } + ), +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/constants.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/constants.ts index b568cba8010cf..bb4dbd838971d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_apps/constants.ts @@ -7,11 +7,7 @@ import type { CreateExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { - ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION, - ENDPOINT_TRUSTED_APPS_LIST_ID, - ENDPOINT_TRUSTED_APPS_LIST_NAME, -} from '@kbn/securitysolution-list-constants'; +import { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants'; export const SEARCHABLE_FIELDS: Readonly = [ `name`, @@ -22,9 +18,9 @@ export const SEARCHABLE_FIELDS: Readonly = [ ]; export const TRUSTED_APPS_EXCEPTION_LIST_DEFINITION: CreateExceptionListSchema = { - name: ENDPOINT_TRUSTED_APPS_LIST_NAME, + name: ENDPOINT_ARTIFACT_LISTS.trustedApps.name, namespace_type: 'agnostic', - description: ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION, - list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, + description: ENDPOINT_ARTIFACT_LISTS.trustedApps.description, + list_id: ENDPOINT_ARTIFACT_LISTS.trustedApps.id, type: ExceptionListTypeEnum.ENDPOINT_TRUSTED_APPS, }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/constants.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/constants.ts new file mode 100644 index 0000000000000..46018ab0ffe08 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/constants.ts @@ -0,0 +1,26 @@ +/* + * 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 { CreateExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; + +export const SEARCHABLE_FIELDS: Readonly = [ + `name`, + `description`, + 'item_id', + `entries.value`, + `entries.entries.value`, +]; + +export const TRUSTED_DEVICES_EXCEPTION_LIST_DEFINITION: CreateExceptionListSchema = { + description: ENDPOINT_ARTIFACT_LISTS.trustedDevices.description, + list_id: ENDPOINT_ARTIFACT_LISTS.trustedDevices.id, + name: ENDPOINT_ARTIFACT_LISTS.trustedDevices.name, + namespace_type: 'agnostic', + type: ExceptionListTypeEnum.ENDPOINT_TRUSTED_DEVICES, +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/index.tsx new file mode 100644 index 0000000000000..478e72cf13e06 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/index.tsx @@ -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 React, { memo } from 'react'; +import { Routes, Route } from '@kbn/shared-ux-router'; +import { MANAGEMENT_ROUTING_TRUSTED_DEVICES_PATH } from '../../common/constants'; +import { NotFoundPage } from '../../../app/404'; +import { TrustedDevicesList } from './view/trusted_devices_list'; + +export const TrustedDevicesContainer = memo(() => { + return ( + + + + + ); +}); + +TrustedDevicesContainer.displayName = 'TrustedDevicesContainer'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/service/api_client.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/service/api_client.ts new file mode 100644 index 0000000000000..ff9707b162f31 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/service/api_client.ts @@ -0,0 +1,64 @@ +/* + * 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 { + CreateExceptionListItemSchema, + ExceptionListItemSchema, + UpdateExceptionListItemSchema, +} from '@kbn/securitysolution-io-ts-list-types'; +import type { HttpStart } from '@kbn/core/public'; +import { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants'; +import type { ConditionEntry } from '../../../../../common/endpoint/types'; +import { + conditionEntriesToEntries, + entriesToConditionEntries, +} from '../../../../common/utils/exception_list_items'; +import { ExceptionsListApiClient } from '../../../services/exceptions_list/exceptions_list_api_client'; +import { TRUSTED_DEVICES_EXCEPTION_LIST_DEFINITION } from '../constants'; + +function readTransform(item: ExceptionListItemSchema): ExceptionListItemSchema { + return { + ...item, + entries: entriesToConditionEntries(item.entries) as ExceptionListItemSchema['entries'], + }; +} + +function writeTransform( + item: T +): T { + return { + ...item, + entries: conditionEntriesToEntries(item.entries as ConditionEntry[], true), + } as T; +} + +/** + * Trusted Devices exceptions Api client class using ExceptionsListApiClient as base class + * It follows the Singleton pattern. + * Please, use the getInstance method instead of creating a new instance when using this implementation. + */ +export class TrustedDevicesApiClient extends ExceptionsListApiClient { + constructor(http: HttpStart) { + super( + http, + ENDPOINT_ARTIFACT_LISTS.trustedDevices.id, + TRUSTED_DEVICES_EXCEPTION_LIST_DEFINITION, + readTransform, + writeTransform + ); + } + + public static getInstance(http: HttpStart): ExceptionsListApiClient { + return super.getInstance( + http, + ENDPOINT_ARTIFACT_LISTS.trustedDevices.id, + TRUSTED_DEVICES_EXCEPTION_LIST_DEFINITION, + readTransform, + writeTransform + ); + } +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/components/artifacts_docs_link.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/components/artifacts_docs_link.tsx new file mode 100644 index 0000000000000..faaef6bf053d3 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/components/artifacts_docs_link.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { EuiLink } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useKibana } from '../../../../../common/lib/kibana'; + +/** + * Link to Trusted Devices documentation + * + * NOTE: This is currently a placeholder for UI development. + * Actual documentation links will be implemented in future phase. + */ +export const TrustedDevicesArtifactsDocsLink = memo(() => { + const { docLinks } = useKibana().services; + + return ( + + + + ); +}); + +TrustedDevicesArtifactsDocsLink.displayName = 'TrustedDevicesArtifactsDocsLink'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/components/form.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/components/form.tsx new file mode 100644 index 0000000000000..c91dec469a66e --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/components/form.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; + +import type { ArtifactFormComponentProps } from '../../../../components/artifact_list_page'; + +export const TrustedDevicesForm = memo( + ({ item, onChange, mode = 'create', disabled = false, error }) => { + return
{'Form placeholder'}
; + } +); + +TrustedDevicesForm.displayName = 'TrustedDevicesForm'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/translations.ts new file mode 100644 index 0000000000000..34eeb40ed3a73 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/translations.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const DETAILS_HEADER = i18n.translate( + 'xpack.securitySolution.trustedDevices.form.detailsHeader', + { + defaultMessage: 'Details', + } +); + +export const DETAILS_HEADER_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.trustedDevices.form.detailsHeaderDescription', + { + defaultMessage: + 'Add a trusted device to improve performance or alleviate compatibility issues.', + } +); + +export const NAME_LABEL = i18n.translate('xpack.securitySolution.trustedDevices.form.nameLabel', { + defaultMessage: 'Name', +}); + +export const DESCRIPTION_LABEL = i18n.translate( + 'xpack.securitySolution.trustedDevices.form.descriptionLabel', + { + defaultMessage: 'Description', + } +); + +export const CONDITIONS_HEADER = i18n.translate( + 'xpack.securitySolution.trustedDevices.form.conditionsHeader', + { + defaultMessage: 'Conditions', + } +); + +export const CONDITIONS_HEADER_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.trustedDevices.form.conditionsHeaderDescription', + { + defaultMessage: + 'Select operating system and add conditions. Availability of conditions may depend on your chosen operating system.', + } +); + +export const SELECT_OS_LABEL = i18n.translate( + 'xpack.securitySolution.trustedDevices.form.selectOsLabel', + { + defaultMessage: 'Select operating system', + } +); + +export const POLICY_SELECT_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.trustedDevices.form.policySelectDescription', + { + defaultMessage: 'Optionally select policies to assign this trusted device to.', + } +); + +export const INPUT_ERRORS = { + name: (itemName: string) => + i18n.translate('xpack.securitySolution.trustedDevices.form.errors.nameRequired', { + defaultMessage: '{itemName} name is required', + values: { itemName }, + }), + entries: i18n.translate('xpack.securitySolution.trustedDevices.form.errors.entriesRequired', { + defaultMessage: 'At least one condition is required', + }), + entriesDuplicateFields: (duplicateFields: string[]) => + i18n.translate('xpack.securitySolution.trustedDevices.form.errors.entriesDuplicateFields', { + defaultMessage: 'Duplicate field(s): {duplicateFields}', + values: { duplicateFields: duplicateFields.join(', ') }, + }), + invalidHash: i18n.translate('xpack.securitySolution.trustedDevices.form.errors.invalidHash', { + defaultMessage: 'Invalid hash value', + }), + invalidPath: i18n.translate('xpack.securitySolution.trustedDevices.form.errors.invalidPath', { + defaultMessage: 'Invalid path', + }), + invalidField: i18n.translate('xpack.securitySolution.trustedDevices.form.errors.invalidField', { + defaultMessage: 'Field entry must have a value', + }), +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/trusted_devices_list.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/trusted_devices_list.tsx new file mode 100644 index 0000000000000..0a8c360403107 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/trusted_devices_list.tsx @@ -0,0 +1,162 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { DocLinks } from '@kbn/doc-links'; +import { EuiLink } from '@elastic/eui'; +import type { ArtifactListPageProps } from '../../../components/artifact_list_page'; +import { ArtifactListPage } from '../../../components/artifact_list_page'; +import { TrustedDevicesApiClient } from '../service/api_client'; +import { TrustedDevicesForm } from './components/form'; +import { useHttp } from '../../../../common/lib/kibana'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { SEARCHABLE_FIELDS } from '../constants'; +import { TrustedDevicesArtifactsDocsLink } from './components/artifacts_docs_link'; + +type TrustedDevicesListProps = Omit< + ArtifactListPageProps, + 'apiClient' | 'ArtifactFormComponent' | 'labels' | 'data-test-subj' +>; + +const TRUSTED_DEVICES_PAGE_LABELS: ArtifactListPageProps['labels'] = { + pageTitle: i18n.translate('xpack.securitySolution.trustedDevices.list.pageTitle', { + defaultMessage: 'Trusted devices', + }), + pageAboutInfo: i18n.translate('xpack.securitySolution.trustedDevices.list.pageAboutInfo', { + defaultMessage: + 'Add a trusted device to improve performance or alleviate compatibility issues with the Elastic Security app.', + }), + pageAddButtonTitle: i18n.translate( + 'xpack.securitySolution.trustedDevices.list.pageAddButtonTitle', + { + defaultMessage: 'Add trusted device', + } + ), + getShowingCountLabel: (total) => + i18n.translate('xpack.securitySolution.trustedDevices.list.showingTotal', { + defaultMessage: + 'Showing {total} {total, plural, one {trusted device} other {trusted devices}}', + values: { total }, + }), + cardActionEditLabel: i18n.translate( + 'xpack.securitySolution.trustedDevices.list.cardActionEditLabel', + { + defaultMessage: 'Edit trusted device', + } + ), + cardActionDeleteLabel: i18n.translate( + 'xpack.securitySolution.trustedDevices.list.cardActionDeleteLabel', + { + defaultMessage: 'Remove from trusted devices list', + } + ), + flyoutCreateTitle: i18n.translate( + 'xpack.securitySolution.trustedDevices.list.flyoutCreateTitle', + { + defaultMessage: 'Add trusted device', + } + ), + flyoutEditTitle: i18n.translate('xpack.securitySolution.trustedDevices.list.flyoutEditTitle', { + defaultMessage: 'Edit trusted device', + }), + flyoutCreateSubmitButtonLabel: i18n.translate( + 'xpack.securitySolution.trustedDevices.list.flyoutCreateSubmitButtonLabel', + { + defaultMessage: 'Add trusted device', + } + ), + flyoutCreateSubmitSuccess: ({ name }) => + i18n.translate('xpack.securitySolution.trustedDevices.list.flyoutCreateSubmitSuccess', { + defaultMessage: '"{name}" has been added to your trusted devices list.', + values: { name }, + }), + flyoutEditSubmitSuccess: ({ name }) => + i18n.translate('xpack.securitySolution.trustedDevices.list.flyoutEditSubmitSuccess', { + defaultMessage: '"{name}" has been updated.', + values: { name }, + }), + flyoutDowngradedLicenseDocsInfo: ( + securitySolutionDocsLinks: DocLinks['securitySolution'] + ): React.ReactNode => { + return ( + <> + + + + + + ); + }, + deleteActionSuccess: (itemName) => + i18n.translate('xpack.securitySolution.trustedDevices.list.deleteActionSuccess', { + defaultMessage: '"{itemName}" has been removed from the trusted devices list', + values: { itemName }, + }), + emptyStateTitleNoEntries: i18n.translate( + 'xpack.securitySolution.trustedDevices.list.emptyStateTitleNoEntries', + { + defaultMessage: 'There are no trusted devices to display.', + } + ), + emptyStateTitle: i18n.translate('xpack.securitySolution.trustedDevices.list.emptyStateTitle', { + defaultMessage: 'Add your first trusted device', + }), + emptyStateInfo: i18n.translate('xpack.securitySolution.trustedDevices.list.emptyStateInfo', { + defaultMessage: + 'There are currently no trusted devices on your Endpoints. Add trusted devices to improve performance or alleviate compatibility issues with the Elastic Security app.', + }), + emptyStatePrimaryButtonLabel: i18n.translate( + 'xpack.securitySolution.trustedDevices.list.emptyStatePrimaryButtonLabel', + { + defaultMessage: 'Add trusted device', + } + ), + searchPlaceholderInfo: i18n.translate( + 'xpack.securitySolution.trustedDevices.list.searchPlaceholderInfo', + { + defaultMessage: 'Search on the fields below: name, description, value', + } + ), +}; + +export const TrustedDevicesList = memo((props) => { + const http = useHttp(); + const isTrustedDevicesEnabled = useIsExperimentalFeatureEnabled('trustedDevicesEnabled'); + + const trustedDevicesListApiClient = TrustedDevicesApiClient.getInstance(http); + + const { canWriteTrustedDevices } = useUserPrivileges().endpointPrivileges; + + if (!isTrustedDevicesEnabled) { + return null; + } + + return ( + } + allowCardDeleteAction={canWriteTrustedDevices} + allowCardEditAction={canWriteTrustedDevices} + allowCardCreateAction={canWriteTrustedDevices} + /> + ); +}); + +TrustedDevicesList.displayName = 'TrustedDevicesList'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/types.ts b/x-pack/solutions/security/plugins/security_solution/public/management/types.ts index ef0e3e56c7285..f18910e06dd76 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/types.ts @@ -28,6 +28,7 @@ export enum AdministrationSubTab { endpoints = 'endpoints', policies = 'policy', trustedApps = 'trusted_apps', + trustedDevices = 'trusted_devices', eventFilters = 'event_filters', hostIsolationExceptions = 'host_isolation_exceptions', blocklist = 'blocklist', diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/common/pli/pli_config.ts b/x-pack/solutions/security/plugins/security_solution_serverless/common/pli/pli_config.ts index 1eb309d137e49..539fa0e098f59 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/common/pli/pli_config.ts +++ b/x-pack/solutions/security/plugins/security_solution_serverless/common/pli/pli_config.ts @@ -68,6 +68,7 @@ export const PLI_PRODUCT_FEATURES: PliProductFeatures = { ProductFeatureKey.endpointPolicyProtections, ProductFeatureKey.endpointArtifactManagement, ProductFeatureKey.endpointExceptions, + ProductFeatureKey.endpointTrustedDevices, ProductFeatureKey.endpointHostIsolationExceptions, ProductFeatureKey.endpointResponseActions, ProductFeatureKey.osqueryAutomatedResponseActions, diff --git a/x-pack/test/security_solution_cypress/cypress/screens/serverless_security_header.ts b/x-pack/test/security_solution_cypress/cypress/screens/serverless_security_header.ts index c3b40beff475a..3bed5383123c6 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/serverless_security_header.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/serverless_security_header.ts @@ -45,6 +45,8 @@ export const POLICIES = '[data-test-subj~="panelNavItem-id-policy"]'; export const TRUSTED_APPS = '[data-test-subj~="panelNavItem-id-trusted_apps"]'; +export const TRUSTED_DEVICES = '[data-test-subj~="panelNavItem-id-trusted_devices"]'; + export const EVENT_FILTERS = '[data-test-subj~="panelNavItem-id-event_filters"]'; export const BLOCKLIST = '[data-test-subj~="panelNavItem-id-blocklist"]'; From 9988f8a108f7fad08d491c58ce7474155c8e9a0f Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Wed, 30 Jul 2025 11:43:40 +0200 Subject: [PATCH 11/26] feat: add execute only access level to device control and update test cases --- .../device_control_protection_level.test.tsx | 3 +- .../device_control_protection_level.tsx | 32 +++++++++++++++---- .../policy_settings_form.tsx | 4 +-- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.test.tsx index f0861c1258922..4f5697c9a01a0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.test.tsx @@ -60,7 +60,8 @@ describe('Policy form DeviceControlProtectionLevel component', () => { it('should render expected options', () => { const { getByTestId } = render(); - + expect(getByTestId('test-execute_onlyRadio')); + expect(getByTestId('test-read_onlyRadio')); expect(getByTestId('test-auditRadio')); expect(getByTestId('test-blockRadio')); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.tsx index 5bc471af04f20..16b0e34c322f1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.tsx @@ -7,7 +7,6 @@ import React, { memo, useCallback, useMemo } from 'react'; import { cloneDeep } from 'lodash'; -import type { EuiFlexItemProps } from '@elastic/eui'; import { EuiRadio, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -36,6 +35,20 @@ const BLOCK_LABEL = i18n.translate( } ); +const READ_ONLY_LABEL = i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.deviceControl.readOnly', + { + defaultMessage: 'Read Only', + } +); + +const EXECUTE_ONLY_LABEL = i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.deviceControl.executeOnly', + { + defaultMessage: 'Execute', + } +); + export type DeviceControlProtectionLevelProps = PolicyFormComponentCommonProps & { osList: ImmutableArray; }; @@ -49,19 +62,24 @@ export const DeviceControlProtectionLevel = memo > = useMemo(() => { return [ + { + id: DeviceControlAccessLevelEnum.execute_only, + label: EXECUTE_ONLY_LABEL, + }, { id: DeviceControlAccessLevelEnum.audit, label: AUDIT_LABEL, - flexGrow: 1, + }, + { + id: DeviceControlAccessLevelEnum.read_only, + label: READ_ONLY_LABEL, }, { id: DeviceControlAccessLevelEnum.block, label: BLOCK_LABEL, - flexGrow: 5, }, ]; }, []); @@ -106,11 +124,11 @@ export const DeviceControlProtectionLevel = memo - + {isEditMode ? ( - radios.map(({ label, id, flexGrow }) => { + radios.map(({ label, id }) => { return ( - + ((props) => { /> - {renderDeviceControlSection()} - @@ -128,6 +126,8 @@ export const PolicySettingsForm = memo((props) => { + + {renderDeviceControlSection()} )} From e84649a5748e12d892c06e4c3cb1a4d4b2617764 Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Wed, 30 Jul 2025 12:30:55 +0200 Subject: [PATCH 12/26] feat: add endpoint_trusted_devices to detection alert schema and common attributes --- .../detection_engine/model/alerts/schema.ts | 1 + .../rule_schema/common_attributes.gen.ts | 1 + .../rule_schema/common_attributes.schema.yaml | 29 ++++++++++--------- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/model/alerts/schema.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/model/alerts/schema.ts index d33a6dd2a98f9..a7567318bf4c1 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/model/alerts/schema.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/model/alerts/schema.ts @@ -271,6 +271,7 @@ type DetectionAlertSchema = { | 'rule_default' | 'endpoint' | 'endpoint_trusted_apps' + | 'endpoint_trusted_devices' | 'endpoint_events' | 'endpoint_host_isolation_exceptions' | 'endpoint_blocklists'; diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts index 51f63b8326fa1..2f0fe2234aa2e 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts @@ -669,6 +669,7 @@ export const ExceptionListType = z.enum([ 'rule_default', 'endpoint', 'endpoint_trusted_apps', + 'endpoint_trusted_devices', 'endpoint_events', 'endpoint_host_isolation_exceptions', 'endpoint_blocklists', diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml index 1080c39e0c248..3dc6e495373b3 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml @@ -475,23 +475,23 @@ components: RelatedIntegration: type: object description: | - Related integration is a potential dependency of a rule. It's assumed that if the user installs - one of the related integrations of a rule, the rule might start to work properly because it will - have source events (generated by this integration) potentially matching the rule's query. + Related integration is a potential dependency of a rule. It's assumed that if the user installs + one of the related integrations of a rule, the rule might start to work properly because it will + have source events (generated by this integration) potentially matching the rule's query. - NOTE: Proper work is not guaranteed, because a related integration, if installed, can be - configured differently or generate data that is not necessarily relevant for this rule. + NOTE: Proper work is not guaranteed, because a related integration, if installed, can be + configured differently or generate data that is not necessarily relevant for this rule. - Related integration is a combination of a Fleet package and (optionally) one of the - package's "integrations" that this package contains. It is represented by 3 properties: + Related integration is a combination of a Fleet package and (optionally) one of the + package's "integrations" that this package contains. It is represented by 3 properties: - - `package`: name of the package (required, unique id) - - `version`: version of the package (required, semver-compatible) - - `integration`: name of the integration of this package (optional, id within the package) + - `package`: name of the package (required, unique id) + - `version`: version of the package (required, semver-compatible) + - `integration`: name of the integration of this package (optional, id within the package) - There are Fleet packages like `windows` that contain only one integration; in this case, - `integration` should be unspecified. There are also packages like `aws` and `azure` that contain - several integrations; in this case, `integration` should be specified. + There are Fleet packages like `windows` that contain only one integration; in this case, + `integration` should be unspecified. There are also packages like `aws` and `azure` that contain + several integrations; in this case, `integration` should be specified. properties: package: $ref: '../../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' @@ -578,7 +578,7 @@ components: - `query` (object, optional): Object containing a query filter which gets applied to an action and determines whether the action should run. - `kql` (string, required): A KQL string. - `filters` (array of objects, required): Array of filter objects, as defined in the `kbn-es-query` package. - + RuleActionParams: type: object description: | @@ -669,6 +669,7 @@ components: - rule_default - endpoint - endpoint_trusted_apps + - endpoint_trusted_devices - endpoint_events - endpoint_host_isolation_exceptions - endpoint_blocklists From c542419483423f16aea6e0014c31d700ff2bfbc2 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 30 Jul 2025 10:48:08 +0000 Subject: [PATCH 13/26] [CI] Auto-commit changed files from 'yarn openapi:bundle' --- ...urity_solution_detections_api_2023_10_31.bundled.schema.yaml | 2 +- ...urity_solution_detections_api_2023_10_31.bundled.schema.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml b/x-pack/solutions/security/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml index 2933ee06ede46..830e92e03d53c 100644 --- a/x-pack/solutions/security/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml @@ -6273,6 +6273,7 @@ components: - rule_default - endpoint - endpoint_trusted_apps + - endpoint_trusted_devices - endpoint_events - endpoint_host_isolation_exceptions - endpoint_blocklists @@ -8527,7 +8528,6 @@ components: gets applied to an action and determines whether the action should run. - `kql` (string, required): A KQL string. - `filters` (array of objects, required): Array of filter objects, as defined in the `kbn-es-query` package. - type: object RuleActionFrequency: description: >- diff --git a/x-pack/solutions/security/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml b/x-pack/solutions/security/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml index 285d8aef28458..c63264d0d3144 100644 --- a/x-pack/solutions/security/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml @@ -5603,6 +5603,7 @@ components: - rule_default - endpoint - endpoint_trusted_apps + - endpoint_trusted_devices - endpoint_events - endpoint_host_isolation_exceptions - endpoint_blocklists @@ -7736,7 +7737,6 @@ components: gets applied to an action and determines whether the action should run. - `kql` (string, required): A KQL string. - `filters` (array of objects, required): Array of filter objects, as defined in the `kbn-es-query` package. - type: object RuleActionFrequency: description: >- From 546814dffe2892a346543c0c7def341127dff500 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 30 Jul 2025 11:11:59 +0000 Subject: [PATCH 14/26] [CI] Auto-commit changed files from 'make api-docs' --- oas_docs/output/kibana.serverless.yaml | 2 +- oas_docs/output/kibana.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 89722125f6a5d..d10ce0fcb3b6d 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -61164,6 +61164,7 @@ components: - rule_default - endpoint - endpoint_trusted_apps + - endpoint_trusted_devices - endpoint_events - endpoint_host_isolation_exceptions - endpoint_blocklists @@ -63096,7 +63097,6 @@ components: - `query` (object, optional): Object containing a query filter which gets applied to an action and determines whether the action should run. - `kql` (string, required): A KQL string. - `filters` (array of objects, required): Array of filter objects, as defined in the `kbn-es-query` package. - type: object Security_Detections_API_RuleActionFrequency: description: The action frequency defines when the action runs (for example, only on rule execution or at specific time intervals). diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index a85b3d89cb33d..4aa991f9f5e64 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -70677,6 +70677,7 @@ components: - rule_default - endpoint - endpoint_trusted_apps + - endpoint_trusted_devices - endpoint_events - endpoint_host_isolation_exceptions - endpoint_blocklists @@ -72730,7 +72731,6 @@ components: - `query` (object, optional): Object containing a query filter which gets applied to an action and determines whether the action should run. - `kql` (string, required): A KQL string. - `filters` (array of objects, required): Array of filter objects, as defined in the `kbn-es-query` package. - type: object Security_Detections_API_RuleActionFrequency: description: The action frequency defines when the action runs (for example, only on rule execution or at specific time intervals). From 40946bdcbd4b94ecf1f5aa514a8fccc0bed95b2c Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 30 Jul 2025 11:14:50 +0000 Subject: [PATCH 15/26] [CI] Auto-commit changed files from 'yarn openapi:generate' --- .../detection_engine/model/rule_schema/common_attributes.gen.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts index 2f0fe2234aa2e..d1e85c5b8e807 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts @@ -576,7 +576,6 @@ export const RuleActionFrequency = z.object({ - `query` (object, optional): Object containing a query filter which gets applied to an action and determines whether the action should run. - `kql` (string, required): A KQL string. - `filters` (array of objects, required): Array of filter objects, as defined in the `kbn-es-query` package. - */ export type RuleActionAlertsFilter = z.infer; From 522fa33ed3fcd52d8f06c28688d05c4ad446c9e7 Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Wed, 30 Jul 2025 14:41:38 +0200 Subject: [PATCH 16/26] feat: add trusted devices to endpoint artifact list types --- .../public/management/cypress/tasks/artifacts.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/cypress/tasks/artifacts.ts b/x-pack/solutions/security/plugins/security_solution/public/management/cypress/tasks/artifacts.ts index 394e43e38ebc1..4a9930222853d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/cypress/tasks/artifacts.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/cypress/tasks/artifacts.ts @@ -57,6 +57,7 @@ const removeExceptionsListPromise = (listId: string) => { const ENDPOINT_ARTIFACT_LIST_TYPES = { [ENDPOINT_ARTIFACT_LISTS.trustedApps.id]: ExceptionListTypeEnum.ENDPOINT, + [ENDPOINT_ARTIFACT_LISTS.trustedDevices.id]: ExceptionListTypeEnum.ENDPOINT_TRUSTED_DEVICES, [ENDPOINT_ARTIFACT_LISTS.eventFilters.id]: ExceptionListTypeEnum.ENDPOINT_EVENTS, [ENDPOINT_ARTIFACT_LISTS.hostIsolationExceptions.id]: ExceptionListTypeEnum.ENDPOINT_HOST_ISOLATION_EXCEPTIONS, From 03a4d9e61cfe28616eb6e212a3905aba0d9d6318 Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Wed, 30 Jul 2025 14:51:21 +0200 Subject: [PATCH 17/26] refactor: remove unnecessary comments from device control components --- .../components/cards/device_control_card.tsx | 4 ---- .../components/device_control_notify_user_option.tsx | 1 - .../components/device_control_protection_level.test.tsx | 1 - .../components/device_control_protection_level.tsx | 3 --- .../components/device_control_setting_card_switch.test.tsx | 3 --- .../view/policy_settings_form/policy_settings_form.tsx | 1 - .../endpoint_management/endpoint_device_control.tsx | 7 ++----- 7 files changed, 2 insertions(+), 18 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/device_control_card.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/device_control_card.tsx index 2544205672213..5d24a1c7de866 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/device_control_card.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/device_control_card.tsx @@ -37,10 +37,6 @@ const DEVICE_CONTROL_OS_VALUES: Immutable = [ PolicyOperatingSystem.mac, ]; -/** - * The Malware Protections form for policy details - * which will configure for all relevant OSes. - */ export const DeviceControlCard = React.memo( ({ policy, onChange, mode = 'edit', 'data-test-subj': dataTestSubj }) => { const getTestId = useTestIdGenerator(dataTestSubj); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_notify_user_option.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_notify_user_option.tsx index 55efa84c1581f..5514d78400617 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_notify_user_option.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_notify_user_option.tsx @@ -47,7 +47,6 @@ export const DeviceControlNotifyUserOption = React.memo( const isEditMode = mode === 'edit'; - // Check if device control is enabled const isDeviceControlEnabled = useMemo(() => { return policy.windows.device_control?.enabled || policy.mac.device_control?.enabled || false; }, [policy]); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.test.tsx index 4f5697c9a01a0..4992f11be55a8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.test.tsx @@ -34,7 +34,6 @@ describe('Policy form DeviceControlProtectionLevel component', () => { const policy = new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0] .config.policy.value; - // Default to block for tests policy.windows.device_control = { enabled: true, usb_storage: DeviceControlAccessLevelEnum.block, diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.tsx index 16b0e34c322f1..c711ba0dd6cf0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.tsx @@ -85,7 +85,6 @@ export const DeviceControlProtectionLevel = memo { - // Check Windows first, then Mac for device_control configuration if (policy.windows.device_control?.usb_storage) { return policy.windows.device_control.usb_storage; } @@ -191,7 +190,6 @@ const DeviceControlAccessRadio = React.memo( const handleRadioChange = useCallback(() => { const newPayload = cloneDeep(policy); - // Update Windows device control if (!newPayload.windows.device_control) { newPayload.windows.device_control = { enabled: true, @@ -201,7 +199,6 @@ const DeviceControlAccessRadio = React.memo( newPayload.windows.device_control.usb_storage = accessLevel; } - // Update Mac device control if (!newPayload.mac.device_control) { newPayload.mac.device_control = { enabled: true, diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_setting_card_switch.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_setting_card_switch.test.tsx index 80f271968d48c..1e0d4caae1fae 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_setting_card_switch.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_setting_card_switch.test.tsx @@ -26,7 +26,6 @@ const setDeviceControlMode = ({ }) => { const enabled = !turnOff; - // Ensure popup and device_control objects exist and assign to constants policy.windows.popup = policy.windows.popup ?? {}; policy.mac.popup = policy.mac.popup ?? {}; if (!policy.windows.device_control) { @@ -44,7 +43,6 @@ const setDeviceControlMode = ({ const windowsDeviceControl = policy.windows.device_control; const macDeviceControl = policy.mac.device_control; - // This logic now mirrors the component's behavior windowsDeviceControl.enabled = enabled; macDeviceControl.enabled = enabled; @@ -54,7 +52,6 @@ const setDeviceControlMode = ({ macDeviceControl.usb_storage = DeviceControlAccessLevelEnum.block; } - // Popups are always aligned with the enabled state policy.windows.popup.device_control = { enabled, message: '' }; policy.mac.popup.device_control = { enabled, message: '' }; }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.tsx index f820f031e689a..ff2338479d6de 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.tsx @@ -52,7 +52,6 @@ export const PolicySettingsForm = memo((props) => { const { eventCollectionDataReductionBannerEnabled, trustedDevicesEnabled } = useEnableExperimental(); - // Helper function to render trusted devices section const renderDeviceControlSection = () => { if (!trustedDevicesEnabled) { return null; diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/endpoint_device_control.tsx b/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/endpoint_device_control.tsx index ce04d7fffc972..b13f0d32959f5 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/endpoint_device_control.tsx +++ b/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/endpoint_device_control.tsx @@ -13,14 +13,14 @@ import styled from '@emotion/styled'; const CARD_TITLE = i18n.translate( 'xpack.securitySolutionServerless.endpointDeviceControl.cardTitle', { - defaultMessage: 'USB device protection', + defaultMessage: 'Device Control', } ); const CARD_MESSAGE = i18n.translate( 'xpack.securitySolutionServerless.endpointDeviceControl.cardMessage', { defaultMessage: - 'To turn on USB device protection, you must add at least Endpoint Complete to your project. ', + 'To turn on Device Control, you must add at least Endpoint Complete to your project. ', } ); const BADGE_TEXT = i18n.translate( @@ -34,9 +34,6 @@ const CardDescription = styled.p` padding: 0 33.3%; `; -/** - * Component displayed when a given product tier is not allowed to use endpoint policy protections. - */ export const EndpointDeviceControl = memo(() => { return ( Date: Wed, 30 Jul 2025 15:23:43 +0200 Subject: [PATCH 18/26] fix: remove outdated upgrade messages from security solution endpoint policy translations --- .../plugins/private/translations/translations/de-DE.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/platform/plugins/private/translations/translations/de-DE.json b/x-pack/platform/plugins/private/translations/translations/de-DE.json index 5b05226930fcf..a570ee0ec3a39 100644 --- a/x-pack/platform/plugins/private/translations/translations/de-DE.json +++ b/x-pack/platform/plugins/private/translations/translations/de-DE.json @@ -37236,7 +37236,6 @@ "xpack.securitySolution.endpoint.policy.details.detectionRulesMessageDocsLink": "Weitere Informationen", "xpack.securitySolution.endpoint.policy.details.eventCollection": "Ereigniserfassung", "xpack.securitySolution.endpoint.policy.details.eventCollectionsEnabled": "{selected} / {total} Ereignissammlungen aktiviert", - "xpack.securitySolution.endpoint.policy.details.lockedCardUpgradeMessage": "Um diesen Schutz zu aktivieren, müssen Sie Ihre Lizenz auf Platinum aktualisieren, eine kostenlose 30-Tage-Testversion starten oder eine {cloudDeploymentLink} auf AWS, GCP oder Azure bereitstellen.", "xpack.securitySolution.endpoint.policy.details.malware": "Malware", "xpack.securitySolution.endpoint.policy.details.memory": "Speicherbedrohung", "xpack.securitySolution.endpoint.policy.details.memory_protection": "Bedrohung des Arbeitsspeichers", @@ -37265,7 +37264,6 @@ "xpack.securitySolution.endpoint.policy.details.updateErrorTitle": "Fehlgeschlagen!", "xpack.securitySolution.endpoint.policy.details.updateSuccessMessage": "Integration {name} wurde aktualisiert.", "xpack.securitySolution.endpoint.policy.details.updateSuccessTitle": "Geschafft!", - "xpack.securitySolution.endpoint.policy.details.upgradeToPlatinum": "Upgrade auf Elastic Platinum", "xpack.securitySolution.endpoint.policy.eventFilters.empty.unassigned.content": "Derzeit sind {policyName} keine Ereignisfilter zugeordnet. Weisen Sie jetzt Ereignisfilter zu oder fügen Sie sie auf der Seite „Ereignisfilter“ hinzu und verwalten Sie sie dort.", "xpack.securitySolution.endpoint.policy.eventFilters.empty.unassigned.noPrivileges.content": "Derzeit sind keine Ereignisfilter zu {policyName}zugewiesen", "xpack.securitySolution.endpoint.policy.eventFilters.empty.unassigned.primaryAction": "Ereignisfilter zuweisen", From 6bf163aa3cbe55f3d70715f3c5e94cc3dd65db32 Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Wed, 30 Jul 2025 19:59:25 +0200 Subject: [PATCH 19/26] feat: add endpoint_trusted_devices to various lists and tests --- .../serverless_upgrade_and_rollback_checks.test.ts.snap | 7 +++++++ .../src/common/lists/index.test.ts | 6 +++--- .../security_solution/public/management/links.test.ts | 3 ++- .../endpoint/migrations/space_awareness_migration.test.ts | 4 +++- .../spaces/trial_license_complete_tier/artifacts.ts | 4 +++- 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/x-pack/platform/plugins/shared/alerting/server/integration_tests/__snapshots__/serverless_upgrade_and_rollback_checks.test.ts.snap b/x-pack/platform/plugins/shared/alerting/server/integration_tests/__snapshots__/serverless_upgrade_and_rollback_checks.test.ts.snap index be93556deb7d6..a38ddb3b81084 100644 --- a/x-pack/platform/plugins/shared/alerting/server/integration_tests/__snapshots__/serverless_upgrade_and_rollback_checks.test.ts.snap +++ b/x-pack/platform/plugins/shared/alerting/server/integration_tests/__snapshots__/serverless_upgrade_and_rollback_checks.test.ts.snap @@ -5754,6 +5754,7 @@ Object { "rule_default", "endpoint", "endpoint_trusted_apps", + "endpoint_trusted_devices", "endpoint_events", "endpoint_host_isolation_exceptions", "endpoint_blocklists", @@ -6425,6 +6426,7 @@ Object { "rule_default", "endpoint", "endpoint_trusted_apps", + "endpoint_trusted_devices", "endpoint_events", "endpoint_host_isolation_exceptions", "endpoint_blocklists", @@ -7159,6 +7161,7 @@ Object { "rule_default", "endpoint", "endpoint_trusted_apps", + "endpoint_trusted_devices", "endpoint_events", "endpoint_host_isolation_exceptions", "endpoint_blocklists", @@ -7811,6 +7814,7 @@ Object { "rule_default", "endpoint", "endpoint_trusted_apps", + "endpoint_trusted_devices", "endpoint_events", "endpoint_host_isolation_exceptions", "endpoint_blocklists", @@ -8519,6 +8523,7 @@ Object { "rule_default", "endpoint", "endpoint_trusted_apps", + "endpoint_trusted_devices", "endpoint_events", "endpoint_host_isolation_exceptions", "endpoint_blocklists", @@ -9228,6 +9233,7 @@ Object { "rule_default", "endpoint", "endpoint_trusted_apps", + "endpoint_trusted_devices", "endpoint_events", "endpoint_host_isolation_exceptions", "endpoint_blocklists", @@ -9937,6 +9943,7 @@ Object { "rule_default", "endpoint", "endpoint_trusted_apps", + "endpoint_trusted_devices", "endpoint_events", "endpoint_host_isolation_exceptions", "endpoint_blocklists", diff --git a/x-pack/solutions/security/packages/kbn-securitysolution-io-ts-list-types/src/common/lists/index.test.ts b/x-pack/solutions/security/packages/kbn-securitysolution-io-ts-list-types/src/common/lists/index.test.ts index f6b4e66290bcd..299e165608575 100644 --- a/x-pack/solutions/security/packages/kbn-securitysolution-io-ts-list-types/src/common/lists/index.test.ts +++ b/x-pack/solutions/security/packages/kbn-securitysolution-io-ts-list-types/src/common/lists/index.test.ts @@ -85,7 +85,7 @@ describe('Lists', () => { const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "rule_default" | "endpoint" | "endpoint_trusted_apps" | "endpoint_events" | "endpoint_host_isolation_exceptions" | "endpoint_blocklists", namespace_type: "agnostic" | "single" |}>"', + 'Invalid value "1" supplied to "Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "rule_default" | "endpoint" | "endpoint_trusted_devices" | "endpoint_trusted_apps" | "endpoint_events" | "endpoint_host_isolation_exceptions" | "endpoint_blocklists", namespace_type: "agnostic" | "single" |}>"', ]); expect(message.schema).toEqual({}); }); @@ -116,8 +116,8 @@ describe('Lists', () => { const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "rule_default" | "endpoint" | "endpoint_trusted_apps" | "endpoint_events" | "endpoint_host_isolation_exceptions" | "endpoint_blocklists", namespace_type: "agnostic" | "single" |}> | undefined)"', - 'Invalid value "[1]" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "rule_default" | "endpoint" | "endpoint_trusted_apps" | "endpoint_events" | "endpoint_host_isolation_exceptions" | "endpoint_blocklists", namespace_type: "agnostic" | "single" |}> | undefined)"', + 'Invalid value "1" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "rule_default" | "endpoint" | "endpoint_trusted_devices" | "endpoint_trusted_apps" | "endpoint_events" | "endpoint_host_isolation_exceptions" | "endpoint_blocklists", namespace_type: "agnostic" | "single" |}> | undefined)"', + 'Invalid value "[1]" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "rule_default" | "endpoint" | "endpoint_trusted_devices" | "endpoint_trusted_apps" | "endpoint_events" | "endpoint_host_isolation_exceptions" | "endpoint_blocklists", namespace_type: "agnostic" | "single" |}> | undefined)"', ]); expect(message.schema).toEqual({}); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/links.test.ts b/x-pack/solutions/security/plugins/security_solution/public/management/links.test.ts index a21e14db8700d..143523a2e4393 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/links.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/links.test.ts @@ -93,7 +93,8 @@ describe('links', () => { SecurityPageName.hostIsolationExceptions, SecurityPageName.policies, SecurityPageName.responseActionsHistory, - SecurityPageName.trustedApps + SecurityPageName.trustedApps, + SecurityPageName.trustedDevices ) ); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/endpoint/migrations/space_awareness_migration.test.ts b/x-pack/solutions/security/plugins/security_solution/server/endpoint/migrations/space_awareness_migration.test.ts index b3831f00b046d..24a9f10fdb394 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/endpoint/migrations/space_awareness_migration.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/endpoint/migrations/space_awareness_migration.test.ts @@ -147,18 +147,20 @@ describe('Space awareness migration', () => { executeFunctionOnStream: expect.any(Function), listId: [ 'endpoint_trusted_apps', + 'endpoint_trusted_devices', 'endpoint_event_filters', 'endpoint_host_isolation_exceptions', 'endpoint_blocklists', 'endpoint_list', ], - namespaceType: ['agnostic', 'agnostic', 'agnostic', 'agnostic', 'agnostic'], + namespaceType: ['agnostic', 'agnostic', 'agnostic', 'agnostic', 'agnostic', 'agnostic'], filter: [ `NOT exception-list-agnostic.attributes.tags:"${buildSpaceOwnerIdTag('*')}"`, `NOT exception-list-agnostic.attributes.tags:"${buildSpaceOwnerIdTag('*')}"`, `NOT exception-list-agnostic.attributes.tags:"${buildSpaceOwnerIdTag('*')}"`, `NOT exception-list-agnostic.attributes.tags:"${buildSpaceOwnerIdTag('*')}"`, `NOT exception-list-agnostic.attributes.tags:"${buildSpaceOwnerIdTag('*')}"`, + `NOT exception-list-agnostic.attributes.tags:"${buildSpaceOwnerIdTag('*')}"`, ], }) ); diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/spaces/trial_license_complete_tier/artifacts.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/spaces/trial_license_complete_tier/artifacts.ts index a0029b151c0cb..963facba81493 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/spaces/trial_license_complete_tier/artifacts.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/spaces/trial_license_complete_tier/artifacts.ts @@ -140,7 +140,9 @@ export default function ({ getService }: FtrProviderContext) { await Promise.allSettled(afterEachDataCleanup.splice(0).map((data) => data.cleanup())); }); - const artifactLists = Object.keys(ENDPOINT_ARTIFACT_LISTS); + const artifactLists = Object.keys(ENDPOINT_ARTIFACT_LISTS).filter( + (k) => k !== 'trustedDevices' + ); // Todo: Enable once trustedDevices are implemented. for (const artifactList of artifactLists) { const listInfo = From 9625f60e54127bdf059c8b50863a8b7d3b18e544 Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Fri, 1 Aug 2025 13:08:43 +0200 Subject: [PATCH 20/26] refactor: update device control access level labels for clarity --- .../models/policy_config_helpers.test.ts | 2 +- .../common/endpoint/types/index.ts | 4 ++-- ...device_control_notify_user_option.test.tsx | 4 ++-- .../device_control_protection_level.tsx | 20 +++++++++---------- .../fleet_integration.test.ts | 12 +++++------ 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts index 6ce1921ac563d..6ac75fd498a25 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts @@ -409,7 +409,7 @@ describe('Policy Config helpers', () => { it('works correctly with custom device_control values', () => { // Set custom device_control values - policy.windows.device_control = { enabled: true, usb_storage: 'block' }; + policy.windows.device_control = { enabled: true, usb_storage: 'deny_all' }; policy.mac.device_control = { enabled: true, usb_storage: 'audit' }; policy.windows.popup.device_control = { enabled: true, message: 'Windows custom message' }; policy.mac.popup.device_control = { enabled: false, message: 'Mac custom message' }; diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/index.ts index 4f0c2649ce0ca..3fe6633399567 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/index.ts @@ -1182,8 +1182,8 @@ export enum AntivirusRegistrationModes { export const DeviceControlAccessLevel = { audit: 'audit', // read and write read_only: 'read_only', - execute_only: 'execute_only', - block: 'block', + execute_only: 'no_execute', + block: 'deny_all', } as const; export type DeviceControlAccessLevel = diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_notify_user_option.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_notify_user_option.test.tsx index 5bca76203f9ff..7127e9c682f32 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_notify_user_option.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_notify_user_option.test.tsx @@ -42,8 +42,8 @@ describe('Policy form DeviceControlNotifyUserOption component', () => { .config.policy.value; // Enable device control and notifications by default - policy.windows.device_control = { enabled: true, usb_storage: 'block' }; - policy.mac.device_control = { enabled: true, usb_storage: 'block' }; + policy.windows.device_control = { enabled: true, usb_storage: 'deny_all' }; + policy.mac.device_control = { enabled: true, usb_storage: 'deny_all' }; policy.windows.popup.device_control = { enabled: true, message: 'hello world' }; policy.mac.popup.device_control = { enabled: true, message: 'hello world' }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.tsx index c711ba0dd6cf0..bfd3d5de026cc 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.tsx @@ -21,10 +21,10 @@ import type { import { DeviceControlAccessLevel as DeviceControlAccessLevelEnum } from '../../../../../../../common/endpoint/types'; import type { DeviceControlOSes } from '../../../types'; -const AUDIT_LABEL = i18n.translate( +const ALLOW_ALL_LABEL = i18n.translate( 'xpack.securitySolution.endpoint.policy.details.deviceControl.allowReadWrite', { - defaultMessage: 'Allow Read and Write', + defaultMessage: 'Allow all', } ); @@ -38,14 +38,14 @@ const BLOCK_LABEL = i18n.translate( const READ_ONLY_LABEL = i18n.translate( 'xpack.securitySolution.endpoint.policy.details.deviceControl.readOnly', { - defaultMessage: 'Read Only', + defaultMessage: 'Read only', } ); -const EXECUTE_ONLY_LABEL = i18n.translate( +const BLOCK_EXECUTE_LABEL = i18n.translate( 'xpack.securitySolution.endpoint.policy.details.deviceControl.executeOnly', { - defaultMessage: 'Execute', + defaultMessage: 'Block execute', } ); @@ -65,18 +65,18 @@ export const DeviceControlProtectionLevel = memo > = useMemo(() => { return [ - { - id: DeviceControlAccessLevelEnum.execute_only, - label: EXECUTE_ONLY_LABEL, - }, { id: DeviceControlAccessLevelEnum.audit, - label: AUDIT_LABEL, + label: ALLOW_ALL_LABEL, }, { id: DeviceControlAccessLevelEnum.read_only, label: READ_ONLY_LABEL, }, + { + id: DeviceControlAccessLevelEnum.execute_only, + label: BLOCK_EXECUTE_LABEL, + }, { id: DeviceControlAccessLevelEnum.block, label: BLOCK_LABEL, diff --git a/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts b/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts index 8d1fd857f060b..b4262631fe29e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts @@ -1079,10 +1079,10 @@ describe('Fleet integrations', () => { const mockPolicy = policyFactory(); // Add some device control settings to test removal if (!mockPolicy.windows.device_control) { - mockPolicy.windows.device_control = { enabled: true, usb_storage: 'block' }; + mockPolicy.windows.device_control = { enabled: true, usb_storage: 'deny_all' }; } else { mockPolicy.windows.device_control.enabled = true; - mockPolicy.windows.device_control.usb_storage = 'block'; + mockPolicy.windows.device_control.usb_storage = 'deny_all'; } const removeDeviceControlSpy = jest.spyOn(PolicyConfigHelpers, 'removeDeviceControl'); @@ -1115,10 +1115,10 @@ describe('Fleet integrations', () => { const mockPolicy = policyFactory(); // Add some device control settings to test removal if (!mockPolicy.windows.device_control) { - mockPolicy.windows.device_control = { enabled: true, usb_storage: 'block' }; + mockPolicy.windows.device_control = { enabled: true, usb_storage: 'deny_all' }; } else { mockPolicy.windows.device_control.enabled = true; - mockPolicy.windows.device_control.usb_storage = 'block'; + mockPolicy.windows.device_control.usb_storage = 'deny_all'; } const removeDeviceControlSpy = jest.spyOn(PolicyConfigHelpers, 'removeDeviceControl'); @@ -1153,10 +1153,10 @@ describe('Fleet integrations', () => { const mockPolicy = policyFactory(); if (!mockPolicy.windows.device_control) { - mockPolicy.windows.device_control = { enabled: true, usb_storage: 'block' }; + mockPolicy.windows.device_control = { enabled: true, usb_storage: 'deny_all' }; } else { mockPolicy.windows.device_control.enabled = true; - mockPolicy.windows.device_control.usb_storage = 'block'; + mockPolicy.windows.device_control.usb_storage = 'deny_all'; } const removeDeviceControlSpy = jest.spyOn(PolicyConfigHelpers, 'removeDeviceControl'); From 807d7d560defd26b81d6c090cab2a1679f392391 Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Fri, 1 Aug 2025 15:26:52 +0200 Subject: [PATCH 21/26] refactor: update access level terminology in DeviceControlProtectionLevel tests --- .../device_control_protection_level.test.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.test.tsx index 4992f11be55a8..a1387424a2df8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.test.tsx @@ -21,11 +21,11 @@ describe('Policy form DeviceControlProtectionLevel component', () => { let render: () => ReturnType; let renderResult: ReturnType; - const clickAccessLevel = async (level: 'audit' | 'block') => { + const clickAccessLevel = async (level: 'audit' | 'deny_all') => { await userEvent.click(renderResult.getByTestId(`test-${level}Radio`).querySelector('label')!); }; - const isAccessLevelChecked = (level: 'audit' | 'block'): boolean => { + const isAccessLevelChecked = (level: 'audit' | 'deny_all'): boolean => { return renderResult.getByTestId(`test-${level}Radio`)!.querySelector('input')!.checked ?? false; }; @@ -59,10 +59,10 @@ describe('Policy form DeviceControlProtectionLevel component', () => { it('should render expected options', () => { const { getByTestId } = render(); - expect(getByTestId('test-execute_onlyRadio')); + expect(getByTestId('test-no_executeRadio')); expect(getByTestId('test-read_onlyRadio')); expect(getByTestId('test-auditRadio')); - expect(getByTestId('test-blockRadio')); + expect(getByTestId('test-deny_allRadio')); }); it('should allow audit mode to be selected', async () => { @@ -72,7 +72,7 @@ describe('Policy form DeviceControlProtectionLevel component', () => { render(); - expect(isAccessLevelChecked('block')).toBe(true); + expect(isAccessLevelChecked('deny_all')).toBe(true); await clickAccessLevel('audit'); @@ -94,7 +94,7 @@ describe('Policy form DeviceControlProtectionLevel component', () => { expect(isAccessLevelChecked('audit')).toBe(true); - await clickAccessLevel('block'); + await clickAccessLevel('deny_all'); expect(formProps.onChange).toHaveBeenCalledWith({ isValid: true, @@ -119,14 +119,16 @@ describe('Policy form DeviceControlProtectionLevel component', () => { render(); expectIsViewOnly(renderResult.getByTestId('test')); - expect(renderResult.getByTestId('test')).toHaveTextContent('Allow Read and Write'); + expect(renderResult.getByTestId('test')).toHaveTextContent( + 'USB storage access levelAllow all' + ); }); it('should not render radio buttons', () => { render(); expect(renderResult.queryByTestId('test-auditRadio')).toBeNull(); - expect(renderResult.queryByTestId('test-blockRadio')).toBeNull(); + expect(renderResult.queryByTestId('test-deny_allRadio')).toBeNull(); }); }); }); From 4b8b59d63cd835242a62f92484cf20eddd31352d Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Mon, 4 Aug 2025 19:37:27 +0200 Subject: [PATCH 22/26] cr --- .../v3_features/kibana_sub_features.ts | 21 +++++++------------ .../common/endpoint/models/policy_config.ts | 4 ++-- .../endpoint/models/policy_config_helpers.ts | 6 +----- .../common/endpoint/types/index.ts | 4 ++-- .../common/experimental_features.ts | 2 +- .../cards/device_control_card.test.tsx | 8 +++++-- .../device_control_protection_level.test.tsx | 9 ++++---- .../device_control_protection_level.tsx | 4 ++-- ...evice_control_setting_card_switch.test.tsx | 4 ++-- .../device_control_setting_card_switch.tsx | 4 ++-- .../policy_settings_form.tsx | 5 ++--- .../fleet_integration.test.ts | 8 +++---- .../fleet_integration/fleet_integration.ts | 2 +- .../handlers/create_default_policy.test.ts | 12 +++++------ .../handlers/create_default_policy.ts | 2 +- 15 files changed, 45 insertions(+), 50 deletions(-) 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 f4202801e306b..5689edcc2fe9f 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 @@ -873,6 +873,14 @@ export const getSecurityV3SubFeaturesMap = ({ SecuritySubFeatureId.trustedApplications, enableSpaceAwarenessIfNeeded(trustedApplicationsSubFeature()), ], + ...((experimentalFeatures.trustedDevicesEnabled + ? [ + [ + SecuritySubFeatureId.trustedDevices, + enableSpaceAwarenessIfNeeded(trustedDevicesSubFeature()), + ], + ] + : []) as Array<[SecuritySubFeatureId, SubFeatureConfig]>), [ SecuritySubFeatureId.hostIsolationExceptionsBasic, enableSpaceAwarenessIfNeeded(hostIsolationExceptionsBasicSubFeature()), @@ -911,19 +919,6 @@ export const getSecurityV3SubFeaturesMap = ({ ]); } - if (experimentalFeatures.trustedDevicesEnabled) { - // place between trusted applications and host isolation exceptions - const trustedAppsIndex = securitySubFeaturesList.findIndex( - ([id]) => id === SecuritySubFeatureId.trustedApplications - ); - if (trustedAppsIndex !== -1) { - securitySubFeaturesList.splice(trustedAppsIndex + 1, 0, [ - SecuritySubFeatureId.trustedDevices, - enableSpaceAwarenessIfNeeded(trustedDevicesSubFeature()), - ]); - } - } - const securitySubFeaturesMap = new Map( securitySubFeaturesList ); diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/models/policy_config.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/models/policy_config.ts index 83d5c6e756a2e..5222b6ef6805e 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/models/policy_config.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/models/policy_config.ts @@ -46,7 +46,7 @@ export const policyFactory = ({ }, device_control: { enabled: true, - usb_storage: DeviceControlAccessLevel.block, + usb_storage: DeviceControlAccessLevel.deny_all, }, malware: { mode: ProtectionModes.prevent, @@ -116,7 +116,7 @@ export const policyFactory = ({ }, device_control: { enabled: true, - usb_storage: DeviceControlAccessLevel.block, + usb_storage: DeviceControlAccessLevel.deny_all, }, behavior_protection: { mode: ProtectionModes.prevent, diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/models/policy_config_helpers.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/models/policy_config_helpers.ts index 1ae22fe142bfa..8d630f174e7a1 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/models/policy_config_helpers.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/models/policy_config_helpers.ts @@ -111,11 +111,6 @@ export const disableProtections = (policy: PolicyConfig): PolicyConfig => { popup: { ...result.windows.popup, ...getDisabledWindowsSpecificPopups(result), - device_control: { - ...result.windows.popup.device_control, - enabled: false, - message: result.windows.popup.device_control?.message || '', - }, }, device_control: { ...result.windows.device_control, @@ -223,6 +218,7 @@ const getDisabledWindowsSpecificPopups = (policy: PolicyConfig) => ({ device_control: { ...policy.windows.popup.device_control, enabled: false, + message: policy.windows.popup.device_control?.message || '', }, }); diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/index.ts index 3fe6633399567..5bfae6ccf5451 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/types/index.ts @@ -1182,8 +1182,8 @@ export enum AntivirusRegistrationModes { export const DeviceControlAccessLevel = { audit: 'audit', // read and write read_only: 'read_only', - execute_only: 'no_execute', - block: 'deny_all', + no_execute: 'no_execute', + deny_all: 'deny_all', } as const; export type DeviceControlAccessLevel = diff --git a/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts b/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts index 7ee4831a58df0..1a4b85e032de5 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts @@ -286,7 +286,7 @@ export const allowedExperimentalValues = Object.freeze({ * Enables Trusted Devices artifact management for device control protections. * Allows users to manage trusted USB and external devices */ - trustedDevicesEnabled: false, + trustedDevices: false, /** * Enables the ability to import and migration dashboards through automatic migration service */ diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/device_control_card.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/device_control_card.test.tsx index 133e03198e96b..564243bbc6db9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/device_control_card.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/device_control_card.test.tsx @@ -150,8 +150,12 @@ describe('Policy Device Control Card', () => { }); it('should display correctly with block protection level', () => { - set(formProps.policy, 'windows.device_control.access_level', DeviceControlAccessLevel.block); - set(formProps.policy, 'mac.device_control.access_level', DeviceControlAccessLevel.block); + set( + formProps.policy, + 'windows.device_control.access_level', + DeviceControlAccessLevel.deny_all + ); + set(formProps.policy, 'mac.device_control.access_level', DeviceControlAccessLevel.deny_all); const { getByTestId } = render(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.test.tsx index a1387424a2df8..4ea5fa6f900e1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.test.tsx @@ -36,11 +36,11 @@ describe('Policy form DeviceControlProtectionLevel component', () => { policy.windows.device_control = { enabled: true, - usb_storage: DeviceControlAccessLevelEnum.block, + usb_storage: DeviceControlAccessLevelEnum.deny_all, }; policy.mac.device_control = { enabled: true, - usb_storage: DeviceControlAccessLevelEnum.block, + usb_storage: DeviceControlAccessLevelEnum.deny_all, }; formProps = { @@ -87,8 +87,9 @@ describe('Policy form DeviceControlProtectionLevel component', () => { formProps.policy.mac.device_control!.usb_storage = DeviceControlAccessLevelEnum.audit; const expectedPolicyUpdate = cloneDeep(formProps.policy); - expectedPolicyUpdate.windows.device_control!.usb_storage = DeviceControlAccessLevelEnum.block; - expectedPolicyUpdate.mac.device_control!.usb_storage = DeviceControlAccessLevelEnum.block; + expectedPolicyUpdate.windows.device_control!.usb_storage = + DeviceControlAccessLevelEnum.deny_all; + expectedPolicyUpdate.mac.device_control!.usb_storage = DeviceControlAccessLevelEnum.deny_all; render(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.tsx index bfd3d5de026cc..128a5daaa1d77 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/device_control_protection_level.tsx @@ -74,11 +74,11 @@ export const DeviceControlProtectionLevel = memo((props) => { const { storage } = useKibana().services; - const { eventCollectionDataReductionBannerEnabled, trustedDevicesEnabled } = - useEnableExperimental(); + const { eventCollectionDataReductionBannerEnabled, trustedDevices } = useEnableExperimental(); const renderDeviceControlSection = () => { - if (!trustedDevicesEnabled) { + if (!trustedDevices) { return null; } diff --git a/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts b/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts index b4262631fe29e..d0a9357a9a6c9 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts @@ -139,7 +139,7 @@ describe('Fleet integrations', () => { licenseService = new LicenseService(); licenseService.start(licenseEmitter); experimentalFeatures = { - trustedDevicesEnabled: true, + trustedDevices: true, } as ExperimentalFeatures; productFeaturesService = endpointAppContextStartContract.productFeaturesService; @@ -1108,9 +1108,9 @@ describe('Fleet integrations', () => { expect(removeDeviceControlSpy).toHaveBeenCalledWith(mockPolicy); }); - it('should remove device control when trustedDevicesEnabled experimental feature is disabled', async () => { + it('should remove device control when trustedDevices experimental feature is disabled', async () => { // @ts-expect-error - experimentalFeatures.trustedDevicesEnabled = false; + experimentalFeatures.trustedDevices = false; const mockPolicy = policyFactory(); // Add some device control settings to test removal @@ -1147,7 +1147,7 @@ describe('Fleet integrations', () => { it('should not remove device control when both features are enabled', async () => { // Reset to enabled states // @ts-expect-error - experimentalFeatures.trustedDevicesEnabled = true; + experimentalFeatures.trustedDevices = true; // @ts-expect-error productFeaturesService = createProductFeaturesServiceMock(ALL_PRODUCT_FEATURE_KEYS); diff --git a/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/fleet_integration.ts b/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/fleet_integration.ts index 39b805372cc7d..144bd71d48ed0 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/fleet_integration.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/fleet_integration.ts @@ -326,7 +326,7 @@ export const getPackagePolicyUpdateCallback = ( } if ( !productFeatures.isEnabled(ProductFeatureSecurityKey.endpointTrustedDevices) || - !experimentalFeatures.trustedDevicesEnabled + !experimentalFeatures.trustedDevices ) { endpointIntegrationData.inputs[0].config.policy.value = removeDeviceControl(newEndpointPackagePolicy); diff --git a/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts b/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts index 84f5d59909959..55891f873fe33 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts @@ -37,7 +37,7 @@ describe('Create Default Policy tests ', () => { let productFeaturesService: ProductFeaturesService; const telemetryConfigProviderMock = createTelemetryConfigProviderMock(); const experimentalFeatures = { - trustedDevicesEnabled: true, + trustedDevices: true, } as ExperimentalFeatures; const createDefaultPolicyCallback = async ( @@ -336,10 +336,10 @@ describe('Create Default Policy tests ', () => { removeDeviceControlSpy.mockRestore(); }); - it('should remove device control when trustedDevicesEnabled experimental feature is disabled', async () => { + it('should remove device control when trustedDevices experimental feature is disabled', async () => { const removeDeviceControlSpy = jest.spyOn(PolicyConfigHelpers, 'removeDeviceControl'); const experimentalFeaturesWithTrustedDevicesDisabled = { - trustedDevicesEnabled: false, + trustedDevices: false, } as ExperimentalFeatures; const createDefaultPolicyCallbackWithDisabledFeature = async ( @@ -367,13 +367,13 @@ describe('Create Default Policy tests ', () => { removeDeviceControlSpy.mockRestore(); }); - it('should remove device control when both endpointTrustedDevices product feature and trustedDevicesEnabled experimental feature are disabled', async () => { + it('should remove device control when both endpointTrustedDevices product feature and experimental feature are disabled', async () => { const removeDeviceControlSpy = jest.spyOn(PolicyConfigHelpers, 'removeDeviceControl'); productFeaturesService = createProductFeaturesServiceMock( ALL_PRODUCT_FEATURE_KEYS.filter((key) => key !== 'endpoint_trusted_devices') ); const experimentalFeaturesWithTrustedDevicesDisabled = { - trustedDevicesEnabled: false, + trustedDevices: false, } as ExperimentalFeatures; const createDefaultPolicyCallbackWithBothDisabled = async ( @@ -401,7 +401,7 @@ describe('Create Default Policy tests ', () => { removeDeviceControlSpy.mockRestore(); }); - it('should NOT remove device control when both endpointTrustedDevices product feature and trustedDevicesEnabled experimental feature are enabled', async () => { + it('should NOT remove device control when both endpointTrustedDevices product feature and trustedDevices experimental feature are enabled', async () => { const removeDeviceControlSpy = jest.spyOn(PolicyConfigHelpers, 'removeDeviceControl'); // Both features are enabled by default in the test setup diff --git a/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts b/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts index 48050961b1a33..41b6db765d12f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts @@ -70,7 +70,7 @@ export const createDefaultPolicy = ( if ( !productFeatures.isEnabled(ProductFeatureSecurityKey.endpointTrustedDevices) || - !experimentalFeatures.trustedDevicesEnabled + !experimentalFeatures.trustedDevices ) { defaultPolicyPerType = removeDeviceControl(defaultPolicyPerType); } From 4f4af331c6b1de3cfef1908394168eaa8441b89e Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Tue, 5 Aug 2025 11:11:20 +0200 Subject: [PATCH 23/26] Refactor trusted devices feature flag references to use unified key --- .../src/security/v3_features/kibana_sub_features.ts | 2 +- .../security_solution/common/experimental_features.ts | 7 +------ .../plugins/security_solution/public/management/links.ts | 2 +- .../security_solution/public/management/pages/index.tsx | 2 +- .../public/management/pages/policy/index.tsx | 2 +- .../endpoint_package_custom_extension.tsx | 4 +--- .../components/endpoint_policy_artifact_cards.tsx | 4 +--- .../management/pages/policy/view/tabs/policy_tabs.tsx | 2 +- .../pages/trusted_devices/view/trusted_devices_list.tsx | 2 +- 9 files changed, 9 insertions(+), 18 deletions(-) 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 5689edcc2fe9f..4cb39c324bb9c 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 @@ -873,7 +873,7 @@ export const getSecurityV3SubFeaturesMap = ({ SecuritySubFeatureId.trustedApplications, enableSpaceAwarenessIfNeeded(trustedApplicationsSubFeature()), ], - ...((experimentalFeatures.trustedDevicesEnabled + ...((experimentalFeatures.trustedDevices ? [ [ SecuritySubFeatureId.trustedDevices, diff --git a/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts b/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts index 78c9050cda55f..21dcc9f4e3fbb 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts @@ -287,16 +287,11 @@ export const allowedExperimentalValues = Object.freeze({ * Allows users to manage trusted USB and external devices */ trustedDevices: false, + /** * Enables the ability to import and migration dashboards through automatic migration service */ automaticDashboardsMigration: false, - - /** - * Enables Trusted Devices artifact management for device control protections. - * Allows users to manage trusted USB and external devices - */ - trustedDevicesEnabled: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/links.ts b/x-pack/solutions/security/plugins/security_solution/public/management/links.ts index dbd5c3c71506b..992d0fbcd5dee 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/links.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/links.ts @@ -153,7 +153,7 @@ export const links: LinkItem = { path: TRUSTED_DEVICES_PATH, skipUrlState: true, hideTimeline: true, - experimentalKey: 'trustedDevicesEnabled', + experimentalKey: 'trustedDevices', capabilities: [`${SECURITY_FEATURE_ID}.readTrustedDevices`], licenseType: 'enterprise', }, diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/index.tsx index 8a96bb656cfb4..14a29fd89dd4f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/index.tsx @@ -101,7 +101,7 @@ export const ManagementContainer = memo(() => { 'securitySolutionNotesDisabled' ); - const trustedDevicesEnabled = useIsExperimentalFeatureEnabled('trustedDevicesEnabled'); + const trustedDevicesEnabled = useIsExperimentalFeatureEnabled('trustedDevices'); const { loading, diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/index.tsx index 25ef9851bff0f..a599806af4a59 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/index.tsx @@ -30,7 +30,7 @@ export const PolicyContainer = memo(() => { const isProtectionUpdatesFeatureEnabled = useIsExperimentalFeatureEnabled( 'protectionUpdatesEnabled' ); - const isTrustedDevicesFeatureEnabled = useIsExperimentalFeatureEnabled('trustedDevicesEnabled'); + const isTrustedDevicesFeatureEnabled = useIsExperimentalFeatureEnabled('trustedDevices'); const isEnterprise = useLicense().isEnterprise(); const isProtectionUpdatesEnabled = isEnterprise && isProtectionUpdatesFeatureEnabled; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx index 2e1c2ff4d54cb..da85549c989dd 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx @@ -146,9 +146,7 @@ export const EndpointPackageCustomExtension = memo { if (loading) { diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/components/endpoint_policy_artifact_cards.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/components/endpoint_policy_artifact_cards.tsx index fe8df6746ffd4..a19a82677efaf 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/components/endpoint_policy_artifact_cards.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/components/endpoint_policy_artifact_cards.tsx @@ -217,9 +217,7 @@ export const EndpointPolicyArtifactCards = memo; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx index ae031b8610a9c..6863d7f077b8a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx @@ -141,7 +141,7 @@ export const PolicyTabs = React.memo(() => { const isProtectionUpdatesFeatureEnabled = useIsExperimentalFeatureEnabled( 'protectionUpdatesEnabled' ); - const isTrustedDevicesFeatureEnabled = useIsExperimentalFeatureEnabled('trustedDevicesEnabled'); + const isTrustedDevicesFeatureEnabled = useIsExperimentalFeatureEnabled('trustedDevices'); const isEnterprise = useLicense().isEnterprise(); const isProtectionUpdatesEnabled = isEnterprise && isProtectionUpdatesFeatureEnabled; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/trusted_devices_list.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/trusted_devices_list.tsx index 0a8c360403107..521a61f48fff7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/trusted_devices_list.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/trusted_devices/view/trusted_devices_list.tsx @@ -134,7 +134,7 @@ const TRUSTED_DEVICES_PAGE_LABELS: ArtifactListPageProps['labels'] = { export const TrustedDevicesList = memo((props) => { const http = useHttp(); - const isTrustedDevicesEnabled = useIsExperimentalFeatureEnabled('trustedDevicesEnabled'); + const isTrustedDevicesEnabled = useIsExperimentalFeatureEnabled('trustedDevices'); const trustedDevicesListApiClient = TrustedDevicesApiClient.getInstance(http); From c84c689a64e4ef105277fd9451d99794bcf7cdbb Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Tue, 5 Aug 2025 12:57:07 +0200 Subject: [PATCH 24/26] Fix TrustedDevicesArtifactCard to use correct artifacts path --- .../endpoint_package_custom_extension.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx index da85549c989dd..4822f25ef182a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx @@ -23,6 +23,7 @@ import { getEventFiltersListPath, getHostIsolationExceptionsListPath, getTrustedAppsListPath, + getTrustedDevicesListPath, } from '../../../../../common/routing'; import { BLOCKLISTS_LABELS, @@ -66,7 +67,7 @@ const TrustedDevicesArtifactCard = memo((p From 5786b5772d93122aa27a6c6b6ec5ebbede98bf79 Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Wed, 6 Aug 2025 09:51:23 +0200 Subject: [PATCH 25/26] Remove unused createEndpointTrustedDevicesList function and its references --- .../create_endpoint_trusted_devices_list.ts | 79 ------------------- .../exception_lists/exception_list_client.ts | 16 ---- 2 files changed, 95 deletions(-) delete mode 100644 x-pack/solutions/security/plugins/lists/server/services/exception_lists/create_endpoint_trusted_devices_list.ts diff --git a/x-pack/solutions/security/plugins/lists/server/services/exception_lists/create_endpoint_trusted_devices_list.ts b/x-pack/solutions/security/plugins/lists/server/services/exception_lists/create_endpoint_trusted_devices_list.ts deleted file mode 100644 index 95a94275d8baa..0000000000000 --- a/x-pack/solutions/security/plugins/lists/server/services/exception_lists/create_endpoint_trusted_devices_list.ts +++ /dev/null @@ -1,79 +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 { SavedObjectsClientContract, SavedObjectsErrorHelpers } from '@kbn/core/server'; -import { v4 as uuidv4 } from 'uuid'; -import type { Version } from '@kbn/securitysolution-io-ts-types'; -import type { ExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { getSavedObjectType } from '@kbn/securitysolution-list-utils'; -import { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants'; - -import { ExceptionListSoSchema } from '../../schemas/saved_objects'; - -import { transformSavedObjectToExceptionList } from './utils'; - -interface CreateEndpointListOptions { - savedObjectsClient: SavedObjectsClientContract; - user: string; - tieBreaker?: string; - version: Version; -} - -/** - * Creates the Endpoint Trusted Devices agnostic list if it does not yet exist - * - * @param savedObjectsClient - * @param user - * @param tieBreaker - * @param version - */ -// TODO: This function is a stub for future implementation of creating the Endpoint Trusted Devices list. It's not being executed in the current codebase. -export const createEndpointTrustedDevicesList = async ({ - savedObjectsClient, - user, - tieBreaker, - version, -}: CreateEndpointListOptions): Promise => { - const savedObjectType = getSavedObjectType({ namespaceType: 'agnostic' }); - const dateNow = new Date().toISOString(); - try { - const savedObject = await savedObjectsClient.create( - savedObjectType, - { - comments: undefined, - created_at: dateNow, - created_by: user, - description: ENDPOINT_ARTIFACT_LISTS.trustedDevices.description, - entries: undefined, - expire_time: undefined, - immutable: false, - item_id: undefined, - list_id: ENDPOINT_ARTIFACT_LISTS.trustedDevices.id, - list_type: 'list', - meta: undefined, - name: ENDPOINT_ARTIFACT_LISTS.trustedDevices.name, - os_types: [], - tags: [], - tie_breaker_id: tieBreaker ?? uuidv4(), - type: 'endpoint', - updated_by: user, - version, - }, - { - // We intentionally hard coding the id so that there can only be one Trusted devices list within the space - id: ENDPOINT_ARTIFACT_LISTS.trustedDevices.id, - } - ); - return transformSavedObjectToExceptionList({ savedObject }); - } catch (err) { - if (SavedObjectsErrorHelpers.isConflictError(err)) { - return null; - } else { - throw err; - } - } -}; diff --git a/x-pack/solutions/security/plugins/lists/server/services/exception_lists/exception_list_client.ts b/x-pack/solutions/security/plugins/lists/server/services/exception_lists/exception_list_client.ts index 4762fc14da95e..c344f6c637f5b 100644 --- a/x-pack/solutions/security/plugins/lists/server/services/exception_lists/exception_list_client.ts +++ b/x-pack/solutions/security/plugins/lists/server/services/exception_lists/exception_list_client.ts @@ -79,7 +79,6 @@ import { findExceptionList } from './find_exception_list'; import { findExceptionListsItem } from './find_exception_list_items'; import { createEndpointList } from './create_endpoint_list'; import { createEndpointTrustedAppsList } from './create_endpoint_trusted_apps_list'; -import { createEndpointTrustedDevicesList } from './create_endpoint_trusted_devices_list'; import { PromiseFromStreams, importExceptions } from './import_exception_list_and_items'; import { transformCreateExceptionListItemOptionsToCreateExceptionListItemSchema, @@ -261,21 +260,6 @@ export class ExceptionListClient { }); }; - /** - * Create the Trusted Devices Agnostic list if it does not yet exist (`null` is returned if it does exist) - * @returns The exception list schema or null if it does not exist - */ - - // TODO: This is a stub for future implementation, it's not being executed in the current codebase. - public createTrustedDevicesList = async (): Promise => { - const { savedObjectsClient, user } = this; - return createEndpointTrustedDevicesList({ - savedObjectsClient, - user, - version: 1, - }); - }; - /** * This is the same as "createListItem" except it applies specifically to the agnostic endpoint list and will * auto-call the "createEndpointList" for you so that you have the best chance of the agnostic endpoint From 099f6df461a3a83c298ec15d7b11698f349f406e Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Thu, 7 Aug 2025 10:43:14 +0200 Subject: [PATCH 26/26] Update data-test-subj attribute in TrustedDevicesPolicyCard to "trustedDevices" --- .../components/endpoint_policy_artifact_cards.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/components/endpoint_policy_artifact_cards.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/components/endpoint_policy_artifact_cards.tsx index a19a82677efaf..229a7ba302690 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/components/endpoint_policy_artifact_cards.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/components/endpoint_policy_artifact_cards.tsx @@ -103,7 +103,7 @@ const TrustedDevicesPolicyCard = memo(({ policyId }) => getArtifactsPath={getArtifactPathHandler} searchableFields={TRUSTED_DEVICES_SEARCHABLE_FIELDS} labels={TRUSTED_DEVICES_LABELS} - data-test-subj="trustedApps" + data-test-subj="trustedDevices" /> ); });